attocode-code-intel 0.1.0__py3-none-any.whl
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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""LLM-based false positive classifier for rule findings.
|
|
2
|
+
|
|
3
|
+
Uses a fast LLM (Haiku) to classify findings as true positive or false
|
|
4
|
+
positive based on code context, rule description, and file purpose.
|
|
5
|
+
Inspired by the Datadog approach to agentic SAST triage.
|
|
6
|
+
|
|
7
|
+
Measured against the rule corpus this is the most precise scorer available
|
|
8
|
+
(P=0.96) and the worst calibrated (ECE=0.28) — a good gate, a poor probability.
|
|
9
|
+
|
|
10
|
+
Reference: https://www.datadoghq.com/blog/using-llms-to-filter-out-false-positives/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from attocode_intel.confidence.redact import redact
|
|
23
|
+
|
|
24
|
+
# (prompt, model, max_tokens) -> completion text
|
|
25
|
+
LLMCaller = Callable[[str, str, int], str]
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# Haiku for cost efficiency (~$0.01 per classification). The id differs by
|
|
30
|
+
# route, so "" means "let _cheap_model() pick for whichever key is present".
|
|
31
|
+
DEFAULT_MODEL = ""
|
|
32
|
+
_OPENROUTER_MODEL = "anthropic/claude-haiku-4.5"
|
|
33
|
+
_ANTHROPIC_MODEL = "claude-haiku-4-5-20251001"
|
|
34
|
+
# Measured: still scaling at 32 (16.9x) — per-call latency is ~1.6s, so there
|
|
35
|
+
# is far more of it to hide than the noul head has.
|
|
36
|
+
_WORKERS = 32
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _default_llm_caller() -> LLMCaller:
|
|
40
|
+
"""The project's shared client. Only importable inside this repo."""
|
|
41
|
+
try:
|
|
42
|
+
from eval.meta_harness._llm_client import call_llm
|
|
43
|
+
except ModuleNotFoundError as exc: # pragma: no cover - depends on checkout
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
"No LLM caller available. Pass llm_caller=..., or run inside the "
|
|
46
|
+
"attocode repo where eval.meta_harness is importable."
|
|
47
|
+
) from exc
|
|
48
|
+
|
|
49
|
+
def call(prompt: str, model: str, max_tokens: int) -> str:
|
|
50
|
+
return call_llm(prompt, model=model, max_tokens=max_tokens)
|
|
51
|
+
|
|
52
|
+
return call
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_env() -> None:
|
|
56
|
+
"""Populate keys from the project .env when running inside the repo."""
|
|
57
|
+
try:
|
|
58
|
+
from eval.meta_harness._llm_client import load_env
|
|
59
|
+
except ModuleNotFoundError: # pragma: no cover - depends on checkout
|
|
60
|
+
return
|
|
61
|
+
load_env()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cheap_model() -> str:
|
|
65
|
+
"""The Haiku id for whichever provider call_llm() will route to."""
|
|
66
|
+
import os
|
|
67
|
+
|
|
68
|
+
return _OPENROUTER_MODEL if os.environ.get("OPENROUTER_API_KEY") else _ANTHROPIC_MODEL
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FPVerdict(StrEnum):
|
|
72
|
+
TRUE_POSITIVE = "true_positive"
|
|
73
|
+
FALSE_POSITIVE = "false_positive"
|
|
74
|
+
UNCERTAIN = "uncertain"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(slots=True)
|
|
78
|
+
class FPClassification:
|
|
79
|
+
"""Result of LLM-based TP/FP classification."""
|
|
80
|
+
|
|
81
|
+
rule_id: str
|
|
82
|
+
file: str
|
|
83
|
+
line: int
|
|
84
|
+
verdict: FPVerdict
|
|
85
|
+
confidence: float # 0.0-1.0
|
|
86
|
+
reasoning: str
|
|
87
|
+
tokens_used: int = 0
|
|
88
|
+
latency_ms: float = 0.0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_CLASSIFICATION_PROMPT = """\
|
|
92
|
+
You are a security code reviewer. Analyze whether the following static analysis finding is a TRUE POSITIVE (real security issue) or FALSE POSITIVE (benign code incorrectly flagged).
|
|
93
|
+
|
|
94
|
+
## Finding
|
|
95
|
+
- **Rule**: {rule_id}
|
|
96
|
+
- **Severity**: {severity}
|
|
97
|
+
- **Description**: {description}
|
|
98
|
+
- **CWE**: {cwe}
|
|
99
|
+
- **File**: {file}:{line}
|
|
100
|
+
|
|
101
|
+
## Code Context
|
|
102
|
+
```
|
|
103
|
+
{code_context}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Matched Line
|
|
107
|
+
```
|
|
108
|
+
{matched_line}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Rule Explanation
|
|
112
|
+
{explanation}
|
|
113
|
+
|
|
114
|
+
## Instructions
|
|
115
|
+
Determine if this is a true positive or false positive. Consider:
|
|
116
|
+
1. Is the flagged pattern actually exploitable in this context?
|
|
117
|
+
2. Is there input validation, sanitization, or other mitigation nearby?
|
|
118
|
+
3. Is this test code, example code, or documentation?
|
|
119
|
+
4. Could the value be controlled by an attacker?
|
|
120
|
+
|
|
121
|
+
Respond with EXACTLY this format (no other text):
|
|
122
|
+
VERDICT: TRUE_POSITIVE or FALSE_POSITIVE or UNCERTAIN
|
|
123
|
+
CONFIDENCE: 0.0 to 1.0
|
|
124
|
+
REASONING: one sentence explanation
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def classify_finding(
|
|
129
|
+
rule_id: str,
|
|
130
|
+
severity: str,
|
|
131
|
+
description: str,
|
|
132
|
+
cwe: str,
|
|
133
|
+
file: str,
|
|
134
|
+
line: int,
|
|
135
|
+
matched_line: str,
|
|
136
|
+
code_context: str,
|
|
137
|
+
explanation: str = "",
|
|
138
|
+
*,
|
|
139
|
+
model: str = DEFAULT_MODEL,
|
|
140
|
+
llm_caller: LLMCaller | None = None,
|
|
141
|
+
) -> FPClassification:
|
|
142
|
+
"""Classify a single finding as TP/FP using an LLM.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
rule_id: The rule that produced the finding.
|
|
146
|
+
severity: Finding severity.
|
|
147
|
+
description: What was found.
|
|
148
|
+
cwe: CWE identifier.
|
|
149
|
+
file: Source file path.
|
|
150
|
+
line: Line number.
|
|
151
|
+
matched_line: The matched source line.
|
|
152
|
+
code_context: ~30 lines of surrounding code.
|
|
153
|
+
explanation: Rule explanation (why it matters).
|
|
154
|
+
model: LLM model to use. Defaults to Haiku on whichever route is keyed.
|
|
155
|
+
llm_caller: Override the provider call. Defaults to the project's
|
|
156
|
+
shared client, which is only importable inside this repo.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
FPClassification with verdict, confidence, and reasoning.
|
|
160
|
+
"""
|
|
161
|
+
import os
|
|
162
|
+
import time
|
|
163
|
+
|
|
164
|
+
_load_env()
|
|
165
|
+
if not (os.environ.get("OPENROUTER_API_KEY") or os.environ.get("ANTHROPIC_API_KEY")):
|
|
166
|
+
return FPClassification(
|
|
167
|
+
rule_id=rule_id, file=file, line=line,
|
|
168
|
+
verdict=FPVerdict.UNCERTAIN, confidence=0.0,
|
|
169
|
+
reasoning="No API key available",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
prompt = _CLASSIFICATION_PROMPT.format(
|
|
173
|
+
rule_id=redact(rule_id),
|
|
174
|
+
severity=redact(severity),
|
|
175
|
+
description=redact(description)[:1000],
|
|
176
|
+
cwe=redact(cwe or "N/A"),
|
|
177
|
+
file=redact(file),
|
|
178
|
+
line=line,
|
|
179
|
+
code_context=redact(code_context)[:2000],
|
|
180
|
+
matched_line=redact(matched_line)[:200],
|
|
181
|
+
explanation=redact(explanation or description)[:2000],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
start = time.monotonic()
|
|
185
|
+
try:
|
|
186
|
+
text = (llm_caller or _default_llm_caller())(
|
|
187
|
+
prompt, model or _cheap_model(), 200,
|
|
188
|
+
)
|
|
189
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
190
|
+
# call_llm returns text only; token counts are not available on this path.
|
|
191
|
+
return _parse_response(rule_id, file, line, text, 0, elapsed)
|
|
192
|
+
|
|
193
|
+
except ImportError:
|
|
194
|
+
return FPClassification(
|
|
195
|
+
rule_id=rule_id, file=file, line=line,
|
|
196
|
+
verdict=FPVerdict.UNCERTAIN, confidence=0.0,
|
|
197
|
+
reasoning="LLM SDK not installed (need openai for OpenRouter)",
|
|
198
|
+
)
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
201
|
+
return FPClassification(
|
|
202
|
+
rule_id=rule_id, file=file, line=line,
|
|
203
|
+
verdict=FPVerdict.UNCERTAIN, confidence=0.0,
|
|
204
|
+
reasoning=f"API error: {exc}",
|
|
205
|
+
latency_ms=elapsed,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _parse_response(
|
|
210
|
+
rule_id: str, file: str, line: int,
|
|
211
|
+
text: str, tokens: int, latency_ms: float,
|
|
212
|
+
) -> FPClassification:
|
|
213
|
+
"""Parse the LLM response into a classification."""
|
|
214
|
+
verdict = FPVerdict.UNCERTAIN
|
|
215
|
+
confidence = 0.5
|
|
216
|
+
reasoning = text.strip()
|
|
217
|
+
|
|
218
|
+
for resp_line in text.strip().splitlines():
|
|
219
|
+
resp_line = resp_line.strip()
|
|
220
|
+
if resp_line.startswith("VERDICT:"):
|
|
221
|
+
v = resp_line.split(":", 1)[1].strip().upper()
|
|
222
|
+
if "TRUE_POSITIVE" in v:
|
|
223
|
+
verdict = FPVerdict.TRUE_POSITIVE
|
|
224
|
+
elif "FALSE_POSITIVE" in v:
|
|
225
|
+
verdict = FPVerdict.FALSE_POSITIVE
|
|
226
|
+
elif resp_line.startswith("CONFIDENCE:"):
|
|
227
|
+
try:
|
|
228
|
+
confidence = float(resp_line.split(":", 1)[1].strip())
|
|
229
|
+
confidence = max(0.0, min(1.0, confidence))
|
|
230
|
+
except ValueError:
|
|
231
|
+
pass
|
|
232
|
+
elif resp_line.startswith("REASONING:"):
|
|
233
|
+
reasoning = resp_line.split(":", 1)[1].strip()
|
|
234
|
+
|
|
235
|
+
return FPClassification(
|
|
236
|
+
rule_id=rule_id,
|
|
237
|
+
file=file,
|
|
238
|
+
line=line,
|
|
239
|
+
verdict=verdict,
|
|
240
|
+
confidence=confidence,
|
|
241
|
+
reasoning=reasoning,
|
|
242
|
+
tokens_used=tokens,
|
|
243
|
+
latency_ms=latency_ms,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Batch classification for benchmark evaluation
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def classify_findings_batch(
|
|
253
|
+
findings_with_context: list[dict[str, Any]],
|
|
254
|
+
*,
|
|
255
|
+
model: str = DEFAULT_MODEL,
|
|
256
|
+
llm_caller: LLMCaller | None = None,
|
|
257
|
+
) -> list[FPClassification]:
|
|
258
|
+
"""Classify multiple findings in batch.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
findings_with_context: List of dicts with keys matching
|
|
262
|
+
classify_finding() parameters.
|
|
263
|
+
model: LLM model.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
List of classifications, one per finding.
|
|
267
|
+
"""
|
|
268
|
+
def one(f: dict[str, Any]) -> FPClassification:
|
|
269
|
+
return classify_finding(
|
|
270
|
+
rule_id=f.get("rule_id", ""),
|
|
271
|
+
severity=f.get("severity", ""),
|
|
272
|
+
description=f.get("description", ""),
|
|
273
|
+
cwe=f.get("cwe", ""),
|
|
274
|
+
file=f.get("file", ""),
|
|
275
|
+
line=f.get("line", 0),
|
|
276
|
+
matched_line=f.get("matched_line", ""),
|
|
277
|
+
code_context=f.get("code_context", ""),
|
|
278
|
+
explanation=f.get("explanation", ""),
|
|
279
|
+
model=model,
|
|
280
|
+
llm_caller=llm_caller,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Concurrent so a wall-clock comparison against other scorers measures the
|
|
284
|
+
# model, not this loop. map() preserves input order.
|
|
285
|
+
with ThreadPoolExecutor(max_workers=_WORKERS) as pool:
|
|
286
|
+
return list(pool.map(one, findings_with_context))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# Scorer interface
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def estimate(findings: list[Any], *, min_confidence: float) -> list[float | None]:
|
|
295
|
+
"""Return p(true positive) per finding, or None where the model had none.
|
|
296
|
+
|
|
297
|
+
``min_confidence`` is unused here: the classifier does not see the
|
|
298
|
+
incumbent constant, unlike the jev scorer which records it for comparison.
|
|
299
|
+
"""
|
|
300
|
+
if not findings:
|
|
301
|
+
return []
|
|
302
|
+
|
|
303
|
+
payload = [
|
|
304
|
+
{
|
|
305
|
+
"rule_id": getattr(f, "rule_id", ""),
|
|
306
|
+
"severity": str(getattr(f, "severity", "")),
|
|
307
|
+
"description": getattr(f, "description", ""),
|
|
308
|
+
"cwe": getattr(f, "cwe", ""),
|
|
309
|
+
"file": f.file,
|
|
310
|
+
"line": f.line,
|
|
311
|
+
"matched_line": getattr(f, "code_snippet", ""),
|
|
312
|
+
"code_context": "\n".join([
|
|
313
|
+
*getattr(f, "context_before", []),
|
|
314
|
+
getattr(f, "code_snippet", ""),
|
|
315
|
+
*getattr(f, "context_after", []),
|
|
316
|
+
]),
|
|
317
|
+
"explanation": getattr(f, "explanation", ""),
|
|
318
|
+
}
|
|
319
|
+
for f in findings
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
out: list[float | None] = []
|
|
323
|
+
for c in classify_findings_batch(payload):
|
|
324
|
+
if c.verdict == FPVerdict.TRUE_POSITIVE:
|
|
325
|
+
out.append(c.confidence)
|
|
326
|
+
elif c.verdict == FPVerdict.FALSE_POSITIVE:
|
|
327
|
+
out.append(1.0 - c.confidence)
|
|
328
|
+
else:
|
|
329
|
+
out.append(None) # no opinion; the constant stands
|
|
330
|
+
return out
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Strip credentials out of code before it leaves the machine.
|
|
2
|
+
|
|
3
|
+
Every scorer in this package sends source context to a third-party API. Repo
|
|
4
|
+
code is exactly where hardcoded keys live, so redaction happens on the way out,
|
|
5
|
+
once, rather than in each scorer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from attocode_intel._internal.integrations.security.patterns import SECRET_PATTERNS
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
def _placeholder(secret: str) -> str:
|
|
18
|
+
"""Describe the secret without disclosing it.
|
|
19
|
+
|
|
20
|
+
A scorer judging a hardcoded-credential finding still needs to know that a
|
|
21
|
+
literal of credential shape is present. Length alone carries that, and the
|
|
22
|
+
surrounding assignment survives, so the evidence is intact and the value
|
|
23
|
+
never leaves the machine.
|
|
24
|
+
"""
|
|
25
|
+
return f"[REDACTED:{len(secret)} chars]"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _blank(match: re.Match[str]) -> str:
|
|
29
|
+
"""Replace the secret itself, keeping the surrounding code readable."""
|
|
30
|
+
whole = match.group(0)
|
|
31
|
+
if match.lastindex: # the pattern captures just the value
|
|
32
|
+
start = match.start(1) - match.start(0)
|
|
33
|
+
end = match.end(1) - match.start(0)
|
|
34
|
+
return whole[:start] + _placeholder(match.group(1)) + whole[end:]
|
|
35
|
+
return _placeholder(whole)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def redact(text: str) -> str:
|
|
39
|
+
"""Return text with anything matching a known secret pattern blanked out."""
|
|
40
|
+
if not text:
|
|
41
|
+
return text
|
|
42
|
+
for pattern in SECRET_PATTERNS:
|
|
43
|
+
text = pattern.pattern.sub(_blank, text)
|
|
44
|
+
return text
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Workspace-scoped scorer settings; never copy repository keys into os.environ."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import tomllib
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from attocode_intel._internal.integrations.feature_flags import registry
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
_project: ContextVar[str] = ContextVar("confidence_project", default="")
|
|
16
|
+
_disabled: ContextVar[bool] = ContextVar("confidence_disabled", default=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def project_dir() -> str:
|
|
20
|
+
from attocode_intel.request_context import current_request
|
|
21
|
+
|
|
22
|
+
request = current_request.get()
|
|
23
|
+
if request is not None and request.source != "local":
|
|
24
|
+
return "" # Hosted repositories cannot configure provider credentials.
|
|
25
|
+
return _project.get() or (request.project_dir if request else "")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@contextmanager
|
|
29
|
+
def workspace(path: str):
|
|
30
|
+
token = _project.set(path)
|
|
31
|
+
try:
|
|
32
|
+
yield
|
|
33
|
+
finally:
|
|
34
|
+
_project.reset(token)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def disabled():
|
|
39
|
+
"""Rule assertion tests must remain deterministic even in a live workspace."""
|
|
40
|
+
token = _disabled.set(True)
|
|
41
|
+
try:
|
|
42
|
+
yield
|
|
43
|
+
finally:
|
|
44
|
+
_disabled.reset(token)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def local_settings() -> dict:
|
|
48
|
+
root = project_dir()
|
|
49
|
+
if not root:
|
|
50
|
+
return {}
|
|
51
|
+
try:
|
|
52
|
+
value = tomllib.loads((Path(root) / ".attocode/config.toml").read_text())["confidence"]
|
|
53
|
+
return value if isinstance(value, dict) else {}
|
|
54
|
+
except (OSError, ValueError, KeyError):
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def selected() -> str:
|
|
59
|
+
if _disabled.get():
|
|
60
|
+
return "off"
|
|
61
|
+
return registry.resolve_with_default("CONFIDENCE", local_settings().get("scorer", "off"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def mode() -> str:
|
|
65
|
+
return registry.resolve_with_default("CONFIDENCE_MODE", local_settings().get("mode", "shadow"))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def environment() -> dict[str, str]:
|
|
69
|
+
"""Credentials/settings for this request, with process environment precedence."""
|
|
70
|
+
from dotenv import dotenv_values
|
|
71
|
+
|
|
72
|
+
root = project_dir()
|
|
73
|
+
values = dotenv_values(Path(root) / ".env") if root else {}
|
|
74
|
+
return {**{k: v for k, v in values.items() if v is not None}, **os.environ}
|