@hoanghainh/komagu 1.8.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/assess/lsa/CHANGELOG.md +6 -0
- package/assess/lsa/VERSION +1 -1
- package/assess/lsa/adapters/build_adapters.py +16 -8
- package/assess/lsa/adapters/codex/README.md +1 -1
- package/assess/lsa/adapters/generic/README.md +1 -1
- package/assess/lsa/examples/eval-run-4/ground-truth.json +823 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/ai-run-stats.json +58 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/coverage.json +85 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings/priv-privilege-001.json +822 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings/priv-privilege-002.json +393 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings.json +4477 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/score.json +120 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/ai-run-stats.json +55 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/coverage.json +50 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings/priv-privilege-001.json +613 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings/priv-privilege-002.json +287 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings.json +4870 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/score.json +107 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/ai-run-stats.json +58 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/coverage.json +92 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings/priv-privilege-001.json +645 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings/priv-privilege-002.json +222 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings.json +4859 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/score.json +121 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/coverage.json +111 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings/priv-privilege-001.json +6 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings/priv-privilege-002.json +344 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings.json +3544 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/score.json +123 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/ai-run-stats.json +50 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/coverage.json +42 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings/priv-privilege-001.json +540 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings/priv-privilege-002.json +238 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings.json +4383 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings.sarif +8729 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/report.ja.md +1911 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/report.md +1911 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/score.json +101 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/stack-profile.json +226 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/ai-run-stats.json +54 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/coverage.json +66 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings/priv-privilege-001.json +440 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings/priv-privilege-002.json +229 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings.json +3424 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/score.json +120 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/ai-run-stats.json +88 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/coverage.json +36 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/findings/priv-privilege-001.json +530 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/findings/priv-privilege-002.json +248 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/findings.json +4766 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/findings.sarif +9326 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/report.ja.md +2011 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/report.md +2011 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/score.json +80 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/stack-profile.json +226 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/ai-run-stats.json +83 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/coverage.json +44 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/findings/priv-privilege-001.json +579 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/findings/priv-privilege-002.json +268 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/findings.json +3913 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/score.json +94 -0
- package/assess/lsa/examples/eval-run-5/ground-truth.json +823 -0
- package/assess/lsa/knowledge/severity-and-cwe.md +1 -1
- package/assess/lsa/prompts/35-privilege-auditor.md +3 -1
- package/assess/lsa/scripts/baseline.py +5 -2
- package/assess/lsa/scripts/findings_to_sarif.py +24 -1
- package/assess/lsa/tests/EVAL-RESULTS.md +156 -0
- package/assess/lsa/tests/score_findings.py +58 -0
- package/assess/lsa/tests/test_adapters.py +21 -2
- package/assess/lsa/tests/test_baseline.py +8 -0
- package/assess/lsa/tests/test_contract_groundwork.py +7 -7
- package/assess/lsa/tests/test_eval_run_4.py +73 -0
- package/assess/lsa/tests/test_eval_run_5.py +73 -0
- package/assess/lsa/tests/test_privilege_tier2.py +30 -0
- package/operate/ai/anchoring.py +130 -0
- package/operate/ai/client.py +13 -0
- package/operate/ai/context.py +105 -0
- package/operate/ai/runner.py +197 -69
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/scan.py +3 -8
- package/operate/cli/commands/test.py +1 -1
- package/operate/cli/env.py +4 -20
- package/operate/cross_user_tester/persona_matrix.py +5 -1
- package/operate/devsecops/pipeline.py +5 -5
- package/operate/exploit/generator.py +5 -1
- package/operate/lsa_sarif/lsa_to_sarif.py +24 -2
- package/operate/lsa_sarif/sarif_findings.py +96 -0
- package/operate/remediator/runner.py +1 -5
- package/operate/remediator/strategies.py +1 -1
- package/operate/tests/test_ai_anchoring.py +184 -0
- package/operate/tests/test_cli_baseline.py +1 -9
- package/operate/tests/test_cli_env.py +8 -29
- package/operate/tests/test_cross_user_pipeline.py +2 -2
- package/operate/tests/test_exploit_and_virtual_patch.py +3 -3
- package/operate/tests/test_headless_ai.py +148 -1
- package/operate/tests/test_npm_package.py +2 -16
- package/operate/tests/test_sarif_from_scan.py +156 -0
- package/package.json +2 -4
- package/remediate/authz/README.md +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/DefaultKomaguAuthorizationService.cs +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/SecurityAuditEvent.cs +2 -2
- package/remediate/authz/golang/authz.go +1 -1
- package/remediate/authz/nodejs/src/service.js +1 -1
- package/remediate/authz/nodejs/tests/test-authz.js +1 -1
- package/remediate/authz/patcher.py +3 -4
- package/remediate/authz/python/komagu_authz/models.py +2 -2
- package/remediate/authz/python/tests/test_fastapi_adapter.py +1 -1
- package/remediate/authz/python/tests/test_fastapi_d5_crosstest.py +1 -1
- package/remediate/authz/python/tests/test_python_authz.py +3 -3
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +1 -1
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/SecurityAuditEvent.kt +1 -2
- package/remediate/authz/tests/test_bola_patcher.py +1 -1
- package/remediate/edge/coraza/rules/coraza-crs-custom.conf +3 -4
- package/remediate/edge/coraza/virtual_patcher.py +4 -6
- package/remediate/edge/docker-compose.yml +1 -1
- package/remediate/ekyc/server/jpki_verifier.py +2 -8
- package/remediate/ekyc/server/tokenization.py +2 -8
- package/remediate/identity/README.md +5 -5
- package/remediate/identity/dotnet/src/Komagu.Security.Identity/KomaguOidcOptions.cs +2 -2
- package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TestTokenGenerator.cs +1 -1
- package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TokenValidationTests.cs +1 -1
- package/remediate/identity/python/komagu_identity/oidc_validator.py +2 -8
- package/remediate/identity/python/tests/test_oidc_validator.py +1 -1
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguOidcProperties.kt +2 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfigurationTest.kt +2 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TestJwtFactory.kt +1 -1
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TokenValidationIntegrationTest.kt +1 -1
- package/remediate/identity/test-env/realm-komagu.json +1 -1
- package/remediate/legacy_shims/coldfusion/KomaguSecurityShim.cfc +5 -5
- package/remediate/query/README.md +1 -1
- package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/DiagnosticIds.cs +1 -1
- package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/Komagu.Security.Query.Analyzers.csproj +1 -1
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/KomaguRuleSetProvider.kt +1 -1
- package/remediate/resilience/dr/dr_manager.py +2 -8
- package/remediate/resilience/dr/runbook.md +2 -2
- package/remediate/resilience/terraform/variables.tf +1 -1
- package/remediate/tenancy/audit/audit_trail.py +2 -8
- package/remediate/tenancy/crypto/byok_manager.py +2 -8
- package/remediate/tenancy/middleware/tenant_context.py +2 -8
- package/remediate/tenancy/sql/rls_isolation.sql +11 -11
- package/remediate/tenancy/sql/schema_per_tenant.sql +7 -7
- package/remediate/tenancy/test_sql_postgres.py +1 -1
- package/remediate/tenancy/tests/test_tenancy.py +4 -4
- package/bin/komainu +0 -16
- package/bin/komainu-legacy.js +0 -16
- package/remediate/ekyc/tests/test_legacy_env.py +0 -33
- package/remediate/identity/python/tests/test_legacy_env.py +0 -33
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessor.kt +0 -60
- package/remediate/identity/spring/src/main/resources/META-INF/spring.factories +0 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessorTest.kt +0 -86
- package/remediate/resilience/tests/test_legacy_env.py +0 -33
- package/remediate/tenancy/tests/test_legacy_env.py +0 -49
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Anchor model-written finding locations to the Tier 1 routes and candidates they refer to (#319).
|
|
2
|
+
|
|
3
|
+
Small local models judge the right route or candidate but often write a wrong or non-existent path
|
|
4
|
+
into the finding (Evaluation Run 4: `routes.js`, `app.py`, `dotnet-mvc/...`). Tier 1 already knows
|
|
5
|
+
where every route handler and query / binding candidate is, so the location comes from there.
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- route findings (`route_id`): the file is the route handler's file; the model's line is kept when
|
|
9
|
+
it falls inside the handler span (handler line up to the next route in the same file, or the end
|
|
10
|
+
of the file), otherwise the handler line is used.
|
|
11
|
+
- candidate findings (`candidate_id`): file and line are the candidate's.
|
|
12
|
+
- evidence entries are re-pointed the same way; entries whose file does not exist and cannot be
|
|
13
|
+
re-pointed are dropped.
|
|
14
|
+
- findings with an unknown `route_id` / `candidate_id` keep their location, get `confidence: low`
|
|
15
|
+
and are reported as unanchored.
|
|
16
|
+
|
|
17
|
+
Standard-library only (Python 3.8+ compatible).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
CANDIDATE_FILES = ("injection.candidates.json", "mass-assignment.candidates.json")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Anchor:
|
|
32
|
+
file: str
|
|
33
|
+
line: int
|
|
34
|
+
end_line: Optional[int] # last line of the handler span; None for an exact candidate site
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class AnchorReport:
|
|
39
|
+
moved: List[str] = field(default_factory=list)
|
|
40
|
+
unanchored: List[str] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read(path: Path) -> Dict[str, Any]:
|
|
44
|
+
try:
|
|
45
|
+
doc = json.loads(path.read_text(encoding="utf-8"))
|
|
46
|
+
return doc if isinstance(doc, dict) else {}
|
|
47
|
+
except (OSError, ValueError):
|
|
48
|
+
return {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _line_count(target_dir: Path, rel_file: str) -> int:
|
|
52
|
+
try:
|
|
53
|
+
return len((target_dir / rel_file).read_text(encoding="utf-8", errors="replace").splitlines())
|
|
54
|
+
except OSError:
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_anchors(out_dir: Path, target_dir: Path) -> Dict[str, Anchor]:
|
|
59
|
+
"""route_id / candidate_id -> Anchor, from OUT/routes/*.json and the Tier 1 candidate files."""
|
|
60
|
+
handlers: List[Tuple[str, int, str]] = []
|
|
61
|
+
for path in sorted((out_dir / "routes").glob("*.json")):
|
|
62
|
+
for route in _read(path).get("routes") or []:
|
|
63
|
+
if not isinstance(route, dict):
|
|
64
|
+
continue
|
|
65
|
+
handler = route.get("handler") or {}
|
|
66
|
+
if route.get("route_id") and handler.get("file") and isinstance(handler.get("line"), int):
|
|
67
|
+
handlers.append((handler["file"], handler["line"], route["route_id"]))
|
|
68
|
+
|
|
69
|
+
anchors: Dict[str, Anchor] = {}
|
|
70
|
+
for rel_file, line, route_id in handlers:
|
|
71
|
+
later = sorted(ln for f, ln, _ in handlers if f == rel_file and ln > line)
|
|
72
|
+
end = later[0] - 1 if later else max(_line_count(target_dir, rel_file), line)
|
|
73
|
+
anchors[route_id] = Anchor(rel_file, line, end)
|
|
74
|
+
|
|
75
|
+
for name in CANDIDATE_FILES:
|
|
76
|
+
for cand in _read(out_dir / name).get("candidates") or []:
|
|
77
|
+
if isinstance(cand, dict) and cand.get("candidate_id") and cand.get("file") and isinstance(cand.get("line"), int):
|
|
78
|
+
anchors[cand["candidate_id"]] = Anchor(cand["file"], cand["line"], None)
|
|
79
|
+
return anchors
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _anchored_line(anchor: Anchor, line: Any) -> int:
|
|
83
|
+
if anchor.end_line is None or not isinstance(line, int):
|
|
84
|
+
return anchor.line
|
|
85
|
+
return line if anchor.line <= line <= anchor.end_line else anchor.line
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _repoint_evidence(evidence: Any, anchor: Anchor, target_dir: Path) -> List[Dict[str, Any]]:
|
|
89
|
+
kept = []
|
|
90
|
+
for ev in evidence if isinstance(evidence, list) else []:
|
|
91
|
+
if not isinstance(ev, dict):
|
|
92
|
+
continue
|
|
93
|
+
rel = str(ev.get("file") or "")
|
|
94
|
+
if rel == anchor.file or (rel and (target_dir / rel).is_file()):
|
|
95
|
+
kept.append(ev)
|
|
96
|
+
elif anchor.end_line is not None and isinstance(ev.get("line"), int) and anchor.line <= ev["line"] <= anchor.end_line:
|
|
97
|
+
kept.append({**ev, "file": anchor.file})
|
|
98
|
+
return kept or [{"file": anchor.file, "line": anchor.line, "why": "Location taken from the Tier 1 inventory"}]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def anchor_finding(finding: Dict[str, Any], anchors: Dict[str, Anchor], target_dir: Path) -> Tuple[Dict[str, Any], Optional[bool]]:
|
|
102
|
+
"""Return (finding with Tier 1 location, moved?); moved is None when the finding has no known anchor."""
|
|
103
|
+
key = finding.get("route_id") or finding.get("candidate_id")
|
|
104
|
+
anchor = anchors.get(key) if key else None
|
|
105
|
+
if anchor is None:
|
|
106
|
+
return {**finding, "confidence": "low"}, None
|
|
107
|
+
loc = finding.get("location") if isinstance(finding.get("location"), dict) else {}
|
|
108
|
+
# a wrong path with a line inside the handler span is most likely the right line in the wrong file
|
|
109
|
+
new_loc = {**loc, "file": anchor.file, "start_line": _anchored_line(anchor, loc.get("start_line"))}
|
|
110
|
+
if anchor.end_line is None:
|
|
111
|
+
new_loc.pop("end_line", None)
|
|
112
|
+
moved = (loc.get("file"), loc.get("start_line")) != (new_loc["file"], new_loc["start_line"])
|
|
113
|
+
evidence = _repoint_evidence(finding.get("evidence"), anchor, target_dir)
|
|
114
|
+
return {**finding, "location": new_loc, "evidence": evidence}, moved
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def anchor_findings(findings: List[Dict[str, Any]], anchors: Dict[str, Anchor], target_dir: Path,
|
|
118
|
+
report: AnchorReport) -> List[Dict[str, Any]]:
|
|
119
|
+
out = []
|
|
120
|
+
for f in findings:
|
|
121
|
+
if not isinstance(f, dict):
|
|
122
|
+
out.append(f)
|
|
123
|
+
continue
|
|
124
|
+
anchored, moved = anchor_finding(f, anchors, target_dir)
|
|
125
|
+
if moved is None:
|
|
126
|
+
report.unanchored.append(str(f.get("finding_id")))
|
|
127
|
+
elif moved:
|
|
128
|
+
report.moved.append(str(f.get("finding_id")))
|
|
129
|
+
out.append(anchored)
|
|
130
|
+
return out
|
package/operate/ai/client.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import re
|
|
10
|
+
import time
|
|
10
11
|
import urllib.error
|
|
11
12
|
import urllib.request
|
|
12
13
|
from typing import Any, Dict, List, Optional
|
|
@@ -30,6 +31,8 @@ class LLMClient:
|
|
|
30
31
|
self.model = model
|
|
31
32
|
self.api_key = api_key or "ollama"
|
|
32
33
|
self.timeout = timeout
|
|
34
|
+
# Running totals across chat() calls, read by the runner for ai-run-stats.json
|
|
35
|
+
self.usage: Dict[str, float] = {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0, "seconds": 0.0}
|
|
33
36
|
|
|
34
37
|
def chat(
|
|
35
38
|
self,
|
|
@@ -54,10 +57,12 @@ class LLMClient:
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
req = urllib.request.Request(self.endpoint, data=data, headers=headers, method="POST")
|
|
60
|
+
started = time.monotonic()
|
|
57
61
|
try:
|
|
58
62
|
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
59
63
|
body = resp.read().decode("utf-8")
|
|
60
64
|
res_json = json.loads(body)
|
|
65
|
+
self._record_usage(res_json.get("usage") or {}, time.monotonic() - started)
|
|
61
66
|
choices = res_json.get("choices", [])
|
|
62
67
|
if not choices:
|
|
63
68
|
raise RuntimeError(f"LLM API returned no choices: {body}")
|
|
@@ -69,6 +74,14 @@ class LLMClient:
|
|
|
69
74
|
except urllib.error.URLError as e:
|
|
70
75
|
raise RuntimeError(f"Failed to connect to LLM endpoint at {self.endpoint}: {e.reason}") from e
|
|
71
76
|
|
|
77
|
+
def _record_usage(self, usage: Dict[str, Any], seconds: float) -> None:
|
|
78
|
+
self.usage = {
|
|
79
|
+
"calls": self.usage["calls"] + 1,
|
|
80
|
+
"prompt_tokens": self.usage["prompt_tokens"] + int(usage.get("prompt_tokens") or 0),
|
|
81
|
+
"completion_tokens": self.usage["completion_tokens"] + int(usage.get("completion_tokens") or 0),
|
|
82
|
+
"seconds": round(self.usage["seconds"] + seconds, 3),
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
@staticmethod
|
|
73
86
|
def extract_json(content: str) -> Any:
|
|
74
87
|
"""Extract and parse JSON from Markdown code blocks or raw text string."""
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Code context for the headless agents: snippets, server-side includes and bound model classes.
|
|
2
|
+
|
|
3
|
+
Standard-library only (Python 3.8+ compatible).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _read_snippet(target_dir: Path, rel_file: str, line: int, radius: int = 15) -> str:
|
|
14
|
+
"""Read a window of code around target line from the audited repository."""
|
|
15
|
+
full_path = target_dir / rel_file
|
|
16
|
+
if not full_path.is_file():
|
|
17
|
+
return f"[File not found: {rel_file}]"
|
|
18
|
+
try:
|
|
19
|
+
lines = full_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
20
|
+
start = max(0, line - radius - 1)
|
|
21
|
+
end = min(len(lines), line + radius)
|
|
22
|
+
numbered = [f"{i + 1:4d} | {lines[i]}" for i in range(start, end)]
|
|
23
|
+
return "\n".join(numbered)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
return f"[Error reading {rel_file}: {e}]"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Server-side includes whose content a verifier must see to judge a control (PHP, Classic ASP, ColdFusion)
|
|
29
|
+
_INCLUDE_RE = re.compile(
|
|
30
|
+
r"(?:require|include)(?:_once)?\s*\(?\s*['\"]([^'\"]+)['\"]"
|
|
31
|
+
r"|<!--\s*#include\s+(?:file|virtual)\s*=\s*\"([^\"]+)\""
|
|
32
|
+
r"|<cfinclude\s+template\s*=\s*\"([^\"]+)\"",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _included_files(target_dir: Path, rel_file: str) -> List[str]:
|
|
38
|
+
"""Repository-relative paths of files that rel_file includes, if they exist."""
|
|
39
|
+
target_dir = target_dir.resolve()
|
|
40
|
+
path = target_dir / rel_file
|
|
41
|
+
if not path.is_file():
|
|
42
|
+
return []
|
|
43
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
44
|
+
found: List[str] = []
|
|
45
|
+
for m in _INCLUDE_RE.finditer(text):
|
|
46
|
+
ref = next(g for g in m.groups() if g)
|
|
47
|
+
candidate = (path.parent / ref).resolve()
|
|
48
|
+
try:
|
|
49
|
+
rel = str(candidate.relative_to(target_dir))
|
|
50
|
+
except ValueError:
|
|
51
|
+
continue
|
|
52
|
+
if candidate.is_file() and rel not in found:
|
|
53
|
+
found.append(rel)
|
|
54
|
+
return found
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_MODEL_EXTENSIONS = (".java", ".cs", ".vb", ".php", ".py", ".js", ".ts", ".rb", ".cfc")
|
|
58
|
+
_NEW_OBJECT_RE = re.compile(r"\bnew\s+([A-Z][A-Za-z0-9_]*)\s*\(")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _model_source(target_dir: Path, name: Optional[str], near_file: str = "", near_line: int = 1) -> Optional[Dict[str, Any]]:
|
|
62
|
+
"""Definition of the bound model class, so the privilege auditor can see its fields (#319).
|
|
63
|
+
|
|
64
|
+
name is the Tier 1 binding target; when it is unknown, the first `new Foo(` near the binding is used.
|
|
65
|
+
"""
|
|
66
|
+
if not name and near_file:
|
|
67
|
+
m = _NEW_OBJECT_RE.search(_read_snippet(target_dir, near_file, near_line, radius=4))
|
|
68
|
+
name = m.group(1) if m else None
|
|
69
|
+
if not name or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
|
|
70
|
+
return None
|
|
71
|
+
class_re = re.compile(rf"\bclass\s+{name}\b")
|
|
72
|
+
# same application and language as the binding: a Node route must not pick up a PHP model of the same name
|
|
73
|
+
suffix = Path(near_file).suffix.lower()
|
|
74
|
+
suffixes = (suffix,) if suffix in _MODEL_EXTENSIONS else _MODEL_EXTENSIONS
|
|
75
|
+
parts = Path(near_file).parts
|
|
76
|
+
root = target_dir / parts[0] if len(parts) > 1 and (target_dir / parts[0]).is_dir() else target_dir
|
|
77
|
+
for path in sorted(root.rglob("*")):
|
|
78
|
+
if path.suffix.lower() not in suffixes or not path.is_file():
|
|
79
|
+
continue
|
|
80
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
81
|
+
for i, text in enumerate(lines):
|
|
82
|
+
if class_re.search(text):
|
|
83
|
+
rel = str(path.relative_to(target_dir))
|
|
84
|
+
return {"class": name, "file": rel, "code": _read_snippet(target_dir, rel, i + 16, radius=15)}
|
|
85
|
+
return {"class": name, "file": None, "code": "[definition not found in the repository]"}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _verification_context(target_dir: Path, finding: Dict[str, Any]) -> Dict[str, Any]:
|
|
89
|
+
"""Code the verifier needs: the finding's site, its file header, evidence lines and included files."""
|
|
90
|
+
loc = finding.get("location") or {}
|
|
91
|
+
rel = str(loc.get("file") or "")
|
|
92
|
+
line = int(loc.get("start_line") or 1)
|
|
93
|
+
ctx: Dict[str, Any] = {"location": _read_snippet(target_dir, rel, line, radius=20)}
|
|
94
|
+
if line > 40:
|
|
95
|
+
ctx["file_header"] = _read_snippet(target_dir, rel, 13, radius=12)
|
|
96
|
+
others = []
|
|
97
|
+
for ev in finding.get("evidence") or []:
|
|
98
|
+
if isinstance(ev, dict) and ev.get("file") and ev.get("file") != rel:
|
|
99
|
+
others.append({"file": ev["file"], "code": _read_snippet(target_dir, str(ev["file"]), int(ev.get("line") or 1), radius=6)})
|
|
100
|
+
if others:
|
|
101
|
+
ctx["evidence"] = others[:3]
|
|
102
|
+
includes = [{"file": inc, "code": _read_snippet(target_dir, inc, 30, radius=30)} for inc in _included_files(target_dir, rel)[:3]]
|
|
103
|
+
if includes:
|
|
104
|
+
ctx["included_files"] = includes
|
|
105
|
+
return ctx
|