ai-code-review-agent / app /services /analysis_service.py
Padmanav's picture
feat: add security depth — secret scanning, dependency vulns, report surface
d860535
Raw
History Blame Contribute Delete
4.93 kB
"""
Shared analysis pipeline, used by both the synchronous /analyze route
and the async Celery task triggered by /analyze/async.
"""
import asyncio
import time
from app.agents import (
bug_detection_agent,
code_review_agent,
report_generator_agent,
repo_analysis_agent,
specialist_review_agent,
test_generation_agent,
)
from app.core.cache import get_cached_report, set_cached_report
from app.core.logger import get_logger
from app.core.metrics import metrics
from app.models.issue import IssueReport
from app.models.report import EngineeringReport
from app.models.review import GeneratedTests, ReviewSuggestions
from app.tools.github_tool import (
clone_repository,
delete_repository,
resolve_remote_head_sha,
)
from app.models.report import SecurityScanResult
from app.tools.ast_parser import scan_repository_security, scan_secrets_regex
from app.tools.dependency_scanner import scan_dependencies
logger = get_logger(__name__)
async def run_full_analysis(
github_url: str, base_sha: str | None = None
) -> EngineeringReport:
"""
Full multi-agent pipeline with a Redis-backed result cache.
If the remote repo's current HEAD SHA can be resolved and a cached
report exists for (github_url, head_sha), that report is returned
immediately — no clone, no agent runs. Otherwise the pipeline runs
normally and the result is cached for subsequent identical requests.
"""
head_sha = resolve_remote_head_sha(github_url)
if head_sha:
cached = get_cached_report(github_url, head_sha)
if cached is not None:
metrics.record_cache_event(hit=True)
logger.info(
"Cache hit — returning cached analysis",
extra={"url": github_url, "head_sha": head_sha},
)
cached.from_cache = True
return cached
metrics.record_cache_event(hit=False)
report = await _run_pipeline(github_url)
if head_sha:
report.repository.head_sha = head_sha
set_cached_report(github_url, head_sha, report)
return report
async def _run_pipeline(github_url: str) -> EngineeringReport:
"""The uncached clone-and-analyze pipeline."""
local_path = None
_start = time.monotonic()
try:
local_path = clone_repository(github_url)
_t = time.monotonic()
metadata = await repo_analysis_agent.run(github_url, local_path)
metrics.record_run("repo_analysis_agent", time.monotonic() - _t, success=True)
_t = time.monotonic()
results = await asyncio.gather(
bug_detection_agent.run(local_path),
test_generation_agent.run(local_path),
code_review_agent.run(local_path, metadata),
specialist_review_agent.run(local_path, metadata, IssueReport()),
return_exceptions=True,
)
any_failed = any(isinstance(r, BaseException) for r in results)
metrics.record_run(
"parallel_agents", time.monotonic() - _t, success=not any_failed
)
issues = results[0] if not isinstance(results[0], BaseException) else IssueReport()
tests = results[1] if not isinstance(results[1], BaseException) else GeneratedTests()
review = (
results[2]
if not isinstance(results[2], BaseException)
else ReviewSuggestions(summary="Review failed.", overall_score=0.0)
)
specialist_review = results[3] if not isinstance(results[3], BaseException) else None
for i, r in enumerate(results):
if isinstance(r, BaseException):
logger.error("Agent failed", extra={"agent_index": i, "error": str(r)})
_t = time.monotonic()
report = await report_generator_agent.run(
metadata, issues, tests, review, specialist_review
)
# Already computed inside bug_detection_agent, but we surface them on the report too
# These are fast (no LLM) so re-running is acceptable
ast_findings = scan_repository_security(local_path)
secret_findings = scan_secrets_regex(local_path)
dep_vulns = scan_dependencies(local_path)
high = sum(1 for f in ast_findings + secret_findings if f.get("severity") == "high")
critical = sum(1 for f in ast_findings if f.get("severity") == "critical")
report.security_scan = SecurityScanResult(
ast_findings=ast_findings,
secret_findings=secret_findings,
dependency_vulnerabilities=dep_vulns,
total_high=high,
total_critical=critical,
)
metrics.record_run(
"report_generator_agent", time.monotonic() - _t, success=True
)
metrics.record_run("full_pipeline", time.monotonic() - _start, success=True)
return report
finally:
if local_path:
delete_repository(local_path)