| import re |
| from collections import Counter |
|
|
| from app.schemas import AgentOutput, CodeChunk, Finding, Severity |
|
|
|
|
| PRINT_CALL = re.compile(r"\bprint\s*\(") |
| LOGGER_CALL = re.compile(r"\b(logging|logger|log)\.(debug|info|warning|error|exception|critical)\s*\(") |
| ROUTE_DECLARATION = re.compile(r"@\w*(app|router)\.(get|post|put|patch|delete|route)\s*\(\s*['\"]([^'\"]+)['\"]") |
| JS_ROUTE_DECLARATION = re.compile(r"\b(app|router)\.(get|post|put|patch|delete)\s*\(\s*['\"]([^'\"]+)['\"]") |
| SENSITIVE_LOG_LINE = re.compile(r"(?i)(print|logging|logger|console)\S*\s*\(.*(password|passwd|secret|token|api[_-]?key)") |
| HEALTH_PATHS = {"/health", "/healthz", "/ready", "/readiness", "/live", "/liveness", "/ping"} |
|
|
|
|
| class ObservabilityAgent: |
| name = "Observability Agent" |
|
|
| async def analyze(self, chunks: list[CodeChunk]) -> AgentOutput: |
| findings: list[Finding] = [] |
| route_paths: set[str] = set() |
| print_counts: Counter[str] = Counter() |
| logger_seen = False |
|
|
| for chunk in chunks: |
| chunk_findings, chunk_routes, chunk_prints, chunk_has_logger = self._scan_chunk(chunk) |
| findings.extend(chunk_findings) |
| route_paths.update(chunk_routes) |
| print_counts[chunk.file_path] += chunk_prints |
| logger_seen = logger_seen or chunk_has_logger |
|
|
| findings.extend(self._print_overuse_findings(chunks, print_counts, logger_seen)) |
| if route_paths and not any(path in HEALTH_PATHS for path in route_paths): |
| findings.append(self._missing_health_finding(chunks[0])) |
|
|
| return AgentOutput( |
| agent_name=self.name, |
| findings=findings, |
| metadata={ |
| "chunks_scanned": len(chunks), |
| "mode": "static-rules", |
| "routes_seen": len(route_paths), |
| "logging_seen": logger_seen, |
| }, |
| ) |
|
|
| def _scan_chunk(self, chunk: CodeChunk) -> tuple[list[Finding], set[str], int, bool]: |
| findings: list[Finding] = [] |
| routes: set[str] = set() |
| print_count = 0 |
| has_logger = False |
|
|
| for offset, line in enumerate(chunk.content.splitlines()): |
| actual_line = chunk.line_start + offset |
| if PRINT_CALL.search(line): |
| print_count += 1 |
| if LOGGER_CALL.search(line): |
| has_logger = True |
| if SENSITIVE_LOG_LINE.search(line): |
| sensitive_term = self._sensitive_term(line) |
| log_snippet = self._snippet(line) |
| findings.append( |
| self._finding( |
| "Sensitive value may be written to logs", |
| Severity.high, |
| chunk, |
| actual_line, |
| f"`{log_snippet}` appears to log credential-like data containing `{sensitive_term}`.", |
| f"Remove `{sensitive_term}` from this log statement and log a masked value or stable identifier instead.", |
| 0.86, |
| why_it_matters="This exact log statement can put sensitive data into terminal output, CI logs, or hosted application logs.", |
| ) |
| ) |
|
|
| routes.update(match.group(3) for match in ROUTE_DECLARATION.finditer(line)) |
| routes.update(match.group(3) for match in JS_ROUTE_DECLARATION.finditer(line)) |
|
|
| return findings, routes, print_count, has_logger |
|
|
| def _print_overuse_findings( |
| self, |
| chunks: list[CodeChunk], |
| print_counts: Counter[str], |
| logger_seen: bool, |
| ) -> list[Finding]: |
| if logger_seen: |
| return [] |
|
|
| findings: list[Finding] = [] |
| first_chunk_by_path = {chunk.file_path: chunk for chunk in chunks} |
| for file_path, count in print_counts.items(): |
| if count < 3: |
| continue |
| chunk = first_chunk_by_path[file_path] |
| findings.append( |
| self._finding( |
| "Print statements used instead of structured logging", |
| Severity.low, |
| chunk, |
| chunk.line_start, |
| f"This file has {count} print statements and no structured logging was detected in the scanned repo.", |
| "Use a logger with levels and structured context such as request_id, route, and operation.", |
| 0.72, |
| why_it_matters=f"`{file_path}` will be harder to filter and correlate in production logs because print output has no severity or structured context.", |
| ) |
| ) |
|
|
| return findings |
|
|
| def _missing_health_finding(self, chunk: CodeChunk) -> Finding: |
| return self._finding( |
| "Web service has routes but no health endpoint detected", |
| Severity.medium, |
| chunk, |
| chunk.line_start, |
| "The scanned code defines web routes but no /health, /ready, /live, or /ping endpoint was detected.", |
| "Add a lightweight health endpoint that returns process readiness and dependency status appropriate for your deployment.", |
| 0.74, |
| why_it_matters="Deployments and uptime checks need a predictable endpoint to tell whether this service process is alive and ready.", |
| ) |
|
|
| def _finding( |
| self, |
| title: str, |
| severity: Severity, |
| chunk: CodeChunk, |
| line_number: int, |
| description: str, |
| suggested_fix: str, |
| confidence: float, |
| why_it_matters: str | None = None, |
| ) -> Finding: |
| return Finding( |
| title=title, |
| severity=severity, |
| file_path=chunk.file_path, |
| line_start=line_number, |
| line_end=line_number, |
| description=description, |
| why_it_matters=why_it_matters |
| or "Without basic observability, production failures are harder to detect, triage, and explain during incidents.", |
| suggested_fix=suggested_fix, |
| agent_source=self.name, |
| category="observability", |
| confidence=confidence, |
| ) |
|
|
| def _sensitive_term(self, line: str) -> str: |
| match = re.search(r"(?i)(password|passwd|secret|token|api[_-]?key)", line) |
| return match.group(1) if match else "secret" |
|
|
| def _snippet(self, line: str, max_length: int = 96) -> str: |
| normalized = " ".join(line.strip().split()) |
| if len(normalized) <= max_length: |
| return normalized |
| return f"{normalized[: max_length - 3]}..." |
|
|