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,382 @@
|
|
|
1
|
+
"""Semgrep rule importer — converts Semgrep YAML rules to attocode format.
|
|
2
|
+
|
|
3
|
+
Handles Semgrep's rule format including:
|
|
4
|
+
- ``pattern``, ``patterns``, ``pattern-either``, ``pattern-not``
|
|
5
|
+
- ``metavariable-regex`` and ``metavariable-comparison``
|
|
6
|
+
- ``fix`` (autofix)
|
|
7
|
+
- ``metadata`` (CWE, confidence, OWASP)
|
|
8
|
+
|
|
9
|
+
Limitations:
|
|
10
|
+
- Semgrep patterns that use ``$X`` metavariables in AST-aware mode
|
|
11
|
+
cannot be directly converted to regex. These are flagged as needing
|
|
12
|
+
the structural tier (ast-grep).
|
|
13
|
+
- ``pattern-inside`` with full AST scope is approximated as line-based.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Semgrep severity -> attocode severity
|
|
25
|
+
_SEVERITY_MAP = {
|
|
26
|
+
"ERROR": "high",
|
|
27
|
+
"WARNING": "medium",
|
|
28
|
+
"INFO": "low",
|
|
29
|
+
"INVENTORY": "info",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Semgrep category hints -> attocode category
|
|
33
|
+
_CATEGORY_MAP = {
|
|
34
|
+
"security": "security",
|
|
35
|
+
"correctness": "correctness",
|
|
36
|
+
"best-practice": "style",
|
|
37
|
+
"performance": "performance",
|
|
38
|
+
"maintainability": "complexity",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _has_ast_metavars(pattern: str) -> bool:
|
|
43
|
+
"""Check if a Semgrep pattern uses AST-aware metavariables.
|
|
44
|
+
|
|
45
|
+
Semgrep metavars like ``$X`` in AST patterns work differently from
|
|
46
|
+
regex patterns — they match entire AST nodes. We can only convert
|
|
47
|
+
simple patterns where ``$X`` maps to ``\\w+`` or similar.
|
|
48
|
+
"""
|
|
49
|
+
# Patterns with ellipsis operators (... in Semgrep) are AST-aware
|
|
50
|
+
if "..." in pattern and "$" in pattern:
|
|
51
|
+
return True
|
|
52
|
+
# Patterns with type constraints or deep patterns
|
|
53
|
+
if "<..." in pattern or "...>" in pattern:
|
|
54
|
+
return True
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _extract_cwe(metadata: dict) -> str:
|
|
59
|
+
"""Extract CWE ID from Semgrep metadata."""
|
|
60
|
+
# Semgrep stores CWE in various places
|
|
61
|
+
cwe = metadata.get("cwe", "")
|
|
62
|
+
if isinstance(cwe, list):
|
|
63
|
+
cwe = cwe[0] if cwe else ""
|
|
64
|
+
cwe = str(cwe)
|
|
65
|
+
|
|
66
|
+
# Normalize to "CWE-NNN" format
|
|
67
|
+
m = re.search(r"CWE-?(\d+)", cwe, re.IGNORECASE)
|
|
68
|
+
if m:
|
|
69
|
+
return f"CWE-{m.group(1)}"
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_confidence(metadata: dict) -> float:
|
|
74
|
+
"""Extract confidence from Semgrep metadata."""
|
|
75
|
+
conf = metadata.get("confidence", "")
|
|
76
|
+
if isinstance(conf, (int, float)):
|
|
77
|
+
return float(conf) if 0 <= conf <= 1 else conf / 100
|
|
78
|
+
conf_str = str(conf).lower()
|
|
79
|
+
if conf_str == "high":
|
|
80
|
+
return 0.9
|
|
81
|
+
elif conf_str == "medium":
|
|
82
|
+
return 0.7
|
|
83
|
+
elif conf_str == "low":
|
|
84
|
+
return 0.5
|
|
85
|
+
return 0.8
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def convert_semgrep_rule(rule: dict[str, Any]) -> dict[str, Any] | None:
|
|
89
|
+
"""Convert a single Semgrep rule to attocode YAML format.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
rule: A single rule dict from Semgrep YAML.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Attocode-format rule dict, or None if unconvertible.
|
|
96
|
+
"""
|
|
97
|
+
rule_id = rule.get("id", "")
|
|
98
|
+
if not rule_id:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
metadata = rule.get("metadata", {})
|
|
102
|
+
severity = _SEVERITY_MAP.get(rule.get("severity", "WARNING"), "medium")
|
|
103
|
+
|
|
104
|
+
# Determine category
|
|
105
|
+
category = "security" # default for Semgrep rules
|
|
106
|
+
for tag in metadata.get("category", []) if isinstance(metadata.get("category"), list) else [str(metadata.get("category", ""))]:
|
|
107
|
+
tag_lower = tag.lower()
|
|
108
|
+
if tag_lower in _CATEGORY_MAP:
|
|
109
|
+
category = _CATEGORY_MAP[tag_lower]
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
# Extract pattern(s)
|
|
113
|
+
attocode_rule: dict[str, Any] = {
|
|
114
|
+
"id": rule_id,
|
|
115
|
+
"name": rule_id.replace("-", " ").replace("_", " ").title()[:60],
|
|
116
|
+
"message": rule.get("message", ""),
|
|
117
|
+
"severity": severity,
|
|
118
|
+
"category": category,
|
|
119
|
+
"confidence": _extract_confidence(metadata),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# CWE
|
|
123
|
+
cwe = _extract_cwe(metadata)
|
|
124
|
+
if cwe:
|
|
125
|
+
attocode_rule["cwe"] = cwe
|
|
126
|
+
|
|
127
|
+
# Languages
|
|
128
|
+
languages = rule.get("languages", [])
|
|
129
|
+
if languages:
|
|
130
|
+
attocode_rule["languages"] = languages
|
|
131
|
+
|
|
132
|
+
# Tags
|
|
133
|
+
tags = metadata.get("tags", [])
|
|
134
|
+
if isinstance(tags, list):
|
|
135
|
+
attocode_rule["tags"] = tags
|
|
136
|
+
|
|
137
|
+
# Handle pattern types
|
|
138
|
+
needs_structural = False
|
|
139
|
+
|
|
140
|
+
if "pattern" in rule:
|
|
141
|
+
pattern = str(rule["pattern"])
|
|
142
|
+
if _has_ast_metavars(pattern):
|
|
143
|
+
needs_structural = True
|
|
144
|
+
attocode_rule["structural_pattern"] = pattern
|
|
145
|
+
# Still provide regex approximation
|
|
146
|
+
regex_approx = _semgrep_to_regex(pattern)
|
|
147
|
+
if regex_approx:
|
|
148
|
+
attocode_rule["pattern"] = regex_approx
|
|
149
|
+
else:
|
|
150
|
+
regex = _semgrep_to_regex(pattern)
|
|
151
|
+
if regex:
|
|
152
|
+
attocode_rule["pattern"] = regex
|
|
153
|
+
else:
|
|
154
|
+
return None # can't convert
|
|
155
|
+
|
|
156
|
+
elif "patterns" in rule:
|
|
157
|
+
# Semgrep composite patterns -> attocode patterns list
|
|
158
|
+
patterns_list = _convert_patterns(rule["patterns"])
|
|
159
|
+
if patterns_list:
|
|
160
|
+
attocode_rule["patterns"] = patterns_list
|
|
161
|
+
else:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
elif "pattern-either" in rule:
|
|
165
|
+
either = rule["pattern-either"]
|
|
166
|
+
converted = []
|
|
167
|
+
for p in either:
|
|
168
|
+
if isinstance(p, str):
|
|
169
|
+
regex = _semgrep_to_regex(p)
|
|
170
|
+
if regex:
|
|
171
|
+
converted.append(regex)
|
|
172
|
+
elif isinstance(p, dict) and "pattern" in p:
|
|
173
|
+
regex = _semgrep_to_regex(str(p["pattern"]))
|
|
174
|
+
if regex:
|
|
175
|
+
converted.append(regex)
|
|
176
|
+
if converted:
|
|
177
|
+
attocode_rule["pattern-either"] = converted
|
|
178
|
+
else:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
return None # no pattern to convert
|
|
183
|
+
|
|
184
|
+
# Metavariable constraints — convert Semgrep format to attocode format
|
|
185
|
+
# Semgrep: {"metavariable": "$VAR", "regex": "..."} or {"metavariable": "$VAR", "comparison": "..."}
|
|
186
|
+
# Attocode: {"VAR": "..."} (key is var name without $)
|
|
187
|
+
for item in rule.get("patterns", []):
|
|
188
|
+
if isinstance(item, dict):
|
|
189
|
+
if "metavariable-regex" in item:
|
|
190
|
+
mv = item["metavariable-regex"]
|
|
191
|
+
if isinstance(mv, dict) and "metavariable" in mv and "regex" in mv:
|
|
192
|
+
var = str(mv["metavariable"]).lstrip("$")
|
|
193
|
+
attocode_rule.setdefault("metavariable-regex", {})[var] = str(mv["regex"])
|
|
194
|
+
elif isinstance(mv, dict):
|
|
195
|
+
# Already in attocode format (key: regex pairs)
|
|
196
|
+
attocode_rule["metavariable-regex"] = mv
|
|
197
|
+
if "metavariable-comparison" in item:
|
|
198
|
+
mv = item["metavariable-comparison"]
|
|
199
|
+
if isinstance(mv, dict) and "metavariable" in mv and "comparison" in mv:
|
|
200
|
+
var = str(mv["metavariable"]).lstrip("$")
|
|
201
|
+
attocode_rule.setdefault("metavariable-comparison", {})[var] = str(mv["comparison"])
|
|
202
|
+
elif isinstance(mv, dict):
|
|
203
|
+
attocode_rule["metavariable-comparison"] = mv
|
|
204
|
+
|
|
205
|
+
# Fix
|
|
206
|
+
fix = rule.get("fix", "")
|
|
207
|
+
if fix:
|
|
208
|
+
attocode_rule["fix"] = {
|
|
209
|
+
"search": rule.get("pattern", ""),
|
|
210
|
+
"replace": fix,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Generate explanation stub from message
|
|
214
|
+
message = rule.get("message", "")
|
|
215
|
+
if message and "explanation" not in attocode_rule:
|
|
216
|
+
attocode_rule["explanation"] = message
|
|
217
|
+
if message and "recommendation" not in attocode_rule:
|
|
218
|
+
attocode_rule["recommendation"] = f"Address: {message[:100]}"
|
|
219
|
+
|
|
220
|
+
# Mark as needing structural tier
|
|
221
|
+
if needs_structural:
|
|
222
|
+
attocode_rule.setdefault("tags", []).append("needs-structural-tier")
|
|
223
|
+
|
|
224
|
+
return attocode_rule
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _semgrep_to_regex(pattern: str) -> str | None:
|
|
228
|
+
"""Convert a Semgrep pattern to a regex pattern.
|
|
229
|
+
|
|
230
|
+
Simple Semgrep patterns (without deep AST matching) can often
|
|
231
|
+
be converted to regex with metavariable support.
|
|
232
|
+
"""
|
|
233
|
+
if not pattern.strip():
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
# Remove leading/trailing whitespace and ellipsis
|
|
237
|
+
p = pattern.strip()
|
|
238
|
+
|
|
239
|
+
# Replace Semgrep ellipsis with regex wildcard
|
|
240
|
+
p = p.replace("...", ".*?")
|
|
241
|
+
|
|
242
|
+
# Escape regex special chars EXCEPT those in metavar patterns
|
|
243
|
+
# We do a targeted escape: escape chars that are regex-special
|
|
244
|
+
# but preserve $VAR metavariables
|
|
245
|
+
result = []
|
|
246
|
+
i = 0
|
|
247
|
+
while i < len(p):
|
|
248
|
+
if p[i] == "$" and i + 1 < len(p) and (p[i + 1].isalpha() or p[i + 1] == "_"):
|
|
249
|
+
# Keep $VAR as-is (metavar)
|
|
250
|
+
j = i + 1
|
|
251
|
+
while j < len(p) and (p[j].isalnum() or p[j] == "_"):
|
|
252
|
+
j += 1
|
|
253
|
+
result.append(p[i:j])
|
|
254
|
+
i = j
|
|
255
|
+
elif p[i] in r"\.+*?[]{}()^|":
|
|
256
|
+
# Escape regex special chars
|
|
257
|
+
if p[i] == "." and i + 2 < len(p) and p[i:i+3] == ".*?":
|
|
258
|
+
result.append(".*?")
|
|
259
|
+
i += 3
|
|
260
|
+
else:
|
|
261
|
+
result.append("\\" + p[i])
|
|
262
|
+
i += 1
|
|
263
|
+
else:
|
|
264
|
+
result.append(p[i])
|
|
265
|
+
i += 1
|
|
266
|
+
|
|
267
|
+
regex = "".join(result)
|
|
268
|
+
|
|
269
|
+
# Verify it compiles
|
|
270
|
+
try:
|
|
271
|
+
re.compile(regex)
|
|
272
|
+
return regex
|
|
273
|
+
except re.error:
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _convert_patterns(patterns: list) -> list[dict[str, str]] | None:
|
|
278
|
+
"""Convert Semgrep patterns list to attocode patterns list."""
|
|
279
|
+
result: list[dict[str, str]] = []
|
|
280
|
+
|
|
281
|
+
for item in patterns:
|
|
282
|
+
if not isinstance(item, dict):
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
if "pattern" in item:
|
|
286
|
+
regex = _semgrep_to_regex(str(item["pattern"]))
|
|
287
|
+
if regex:
|
|
288
|
+
result.append({"pattern": regex})
|
|
289
|
+
|
|
290
|
+
elif "pattern-not" in item:
|
|
291
|
+
regex = _semgrep_to_regex(str(item["pattern-not"]))
|
|
292
|
+
if regex:
|
|
293
|
+
result.append({"pattern-not": regex})
|
|
294
|
+
|
|
295
|
+
elif "pattern-inside" in item:
|
|
296
|
+
regex = _semgrep_to_regex(str(item["pattern-inside"]))
|
|
297
|
+
if regex:
|
|
298
|
+
result.append({"pattern-inside": regex})
|
|
299
|
+
|
|
300
|
+
elif "pattern-not-inside" in item:
|
|
301
|
+
regex = _semgrep_to_regex(str(item["pattern-not-inside"]))
|
|
302
|
+
if regex:
|
|
303
|
+
result.append({"pattern-not-inside": regex})
|
|
304
|
+
|
|
305
|
+
elif "pattern-either" in item:
|
|
306
|
+
either_list = item["pattern-either"]
|
|
307
|
+
converted = []
|
|
308
|
+
for p in either_list:
|
|
309
|
+
if isinstance(p, str):
|
|
310
|
+
regex = _semgrep_to_regex(p)
|
|
311
|
+
if regex:
|
|
312
|
+
converted.append(regex)
|
|
313
|
+
elif isinstance(p, dict) and "pattern" in p:
|
|
314
|
+
regex = _semgrep_to_regex(str(p["pattern"]))
|
|
315
|
+
if regex:
|
|
316
|
+
converted.append(regex)
|
|
317
|
+
if converted:
|
|
318
|
+
result.append({"pattern-either": converted})
|
|
319
|
+
|
|
320
|
+
return result if result else None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ---------------------------------------------------------------------------
|
|
324
|
+
# Batch conversion
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def convert_semgrep_file(yaml_content: str) -> tuple[list[dict], list[str]]:
|
|
329
|
+
"""Convert a Semgrep YAML file (possibly with multiple rules) to attocode format.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
yaml_content: Raw YAML content from a Semgrep rule file.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
(converted_rules, errors) — list of attocode rule dicts and error messages.
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
import yaml # type: ignore[import-untyped]
|
|
339
|
+
data = yaml.safe_load(yaml_content)
|
|
340
|
+
except Exception as exc:
|
|
341
|
+
return [], [f"Failed to parse YAML: {exc}"]
|
|
342
|
+
|
|
343
|
+
if not isinstance(data, dict) or "rules" not in data:
|
|
344
|
+
return [], ["Expected Semgrep format with top-level 'rules' key"]
|
|
345
|
+
|
|
346
|
+
converted: list[dict] = []
|
|
347
|
+
errors: list[str] = []
|
|
348
|
+
|
|
349
|
+
for i, rule in enumerate(data["rules"]):
|
|
350
|
+
if not isinstance(rule, dict):
|
|
351
|
+
errors.append(f"Rule at index {i}: expected dict")
|
|
352
|
+
continue
|
|
353
|
+
result = convert_semgrep_rule(rule)
|
|
354
|
+
if result:
|
|
355
|
+
converted.append(result)
|
|
356
|
+
else:
|
|
357
|
+
errors.append(f"Rule '{rule.get('id', f'index-{i}')}': could not convert")
|
|
358
|
+
|
|
359
|
+
return converted, errors
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def convert_semgrep_to_yaml(yaml_content: str) -> str:
|
|
363
|
+
"""Convert Semgrep YAML to attocode YAML string.
|
|
364
|
+
|
|
365
|
+
Convenience function for the MCP tool.
|
|
366
|
+
"""
|
|
367
|
+
converted, errors = convert_semgrep_file(yaml_content)
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
import yaml # type: ignore[import-untyped]
|
|
371
|
+
except ImportError:
|
|
372
|
+
return "Error: PyYAML not installed"
|
|
373
|
+
|
|
374
|
+
parts: list[str] = []
|
|
375
|
+
if converted:
|
|
376
|
+
parts.append(yaml.dump(converted, default_flow_style=False, sort_keys=False))
|
|
377
|
+
if errors:
|
|
378
|
+
parts.append("\n# Conversion errors:")
|
|
379
|
+
for e in errors:
|
|
380
|
+
parts.append(f"# {e}")
|
|
381
|
+
|
|
382
|
+
return "\n".join(parts) if parts else "No rules converted."
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Rule loader — loads rules from builtins, YAML files, and packs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from attocode_intel.rules.metavar import compile_metavar_pattern, has_metavars
|
|
10
|
+
from attocode_intel.rules.model import (
|
|
11
|
+
AutoFix,
|
|
12
|
+
FewShotExample,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleSeverity,
|
|
15
|
+
RuleSource,
|
|
16
|
+
RuleTier,
|
|
17
|
+
UnifiedRule,
|
|
18
|
+
from_bug_pattern,
|
|
19
|
+
from_security_pattern,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Category mapping for YAML rules
|
|
25
|
+
_YAML_CATEGORY_MAP: dict[str, RuleCategory] = {
|
|
26
|
+
"correctness": RuleCategory.CORRECTNESS,
|
|
27
|
+
"suspicious": RuleCategory.SUSPICIOUS,
|
|
28
|
+
"complexity": RuleCategory.COMPLEXITY,
|
|
29
|
+
"performance": RuleCategory.PERFORMANCE,
|
|
30
|
+
"style": RuleCategory.STYLE,
|
|
31
|
+
"security": RuleCategory.SECURITY,
|
|
32
|
+
"deprecated": RuleCategory.DEPRECATED,
|
|
33
|
+
# Legacy compat (must match from_bug_pattern and from_security_pattern mappings)
|
|
34
|
+
"anti_pattern": RuleCategory.SUSPICIOUS,
|
|
35
|
+
"secret": RuleCategory.SECURITY,
|
|
36
|
+
"logic_error": RuleCategory.CORRECTNESS,
|
|
37
|
+
"error_handling": RuleCategory.CORRECTNESS,
|
|
38
|
+
"edge_case": RuleCategory.SUSPICIOUS,
|
|
39
|
+
"concurrency": RuleCategory.SUSPICIOUS,
|
|
40
|
+
"resource_leak": RuleCategory.CORRECTNESS,
|
|
41
|
+
"type_safety": RuleCategory.CORRECTNESS,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_VALID_SEVERITIES = frozenset(s.value for s in RuleSeverity)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_builtin_rules() -> list[UnifiedRule]:
|
|
48
|
+
"""Load builtin rules from bug_finder and security patterns."""
|
|
49
|
+
rules: list[UnifiedRule] = []
|
|
50
|
+
|
|
51
|
+
# 1. Bug finder patterns
|
|
52
|
+
try:
|
|
53
|
+
from attocode_intel.bug_finder import _PATTERNS
|
|
54
|
+
|
|
55
|
+
for regex, cat, sev, desc, conf in _PATTERNS:
|
|
56
|
+
try:
|
|
57
|
+
rule = from_bug_pattern(regex, cat.value, sev.value, desc, conf)
|
|
58
|
+
rules.append(rule)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
logger.warning("Failed to adapt builtin bug pattern '%s': %s", desc[:40], exc)
|
|
61
|
+
except ImportError:
|
|
62
|
+
logger.debug("bug_finder not available, skipping builtin bug rules")
|
|
63
|
+
|
|
64
|
+
# 2. Security patterns (secrets + anti-patterns)
|
|
65
|
+
try:
|
|
66
|
+
from attocode_intel._internal.integrations.security.patterns import (
|
|
67
|
+
ANTI_PATTERNS,
|
|
68
|
+
SECRET_PATTERNS,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
for pat in SECRET_PATTERNS:
|
|
72
|
+
try:
|
|
73
|
+
rules.append(from_security_pattern(pat))
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
logger.warning("Failed to adapt security pattern '%s': %s", getattr(pat, "name", "?"), exc)
|
|
76
|
+
for pat in ANTI_PATTERNS:
|
|
77
|
+
try:
|
|
78
|
+
rules.append(from_security_pattern(pat))
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
logger.warning("Failed to adapt security pattern '%s': %s", getattr(pat, "name", "?"), exc)
|
|
81
|
+
except ImportError:
|
|
82
|
+
logger.debug("security patterns not available, skipping builtin security rules")
|
|
83
|
+
|
|
84
|
+
logger.info("Loaded %d builtin rules", len(rules))
|
|
85
|
+
return rules
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_yaml_rules(
|
|
89
|
+
rules_dir: str | Path,
|
|
90
|
+
*,
|
|
91
|
+
source: RuleSource = RuleSource.USER,
|
|
92
|
+
pack: str = "",
|
|
93
|
+
) -> list[UnifiedRule]:
|
|
94
|
+
"""Load rules from YAML files in a directory.
|
|
95
|
+
|
|
96
|
+
Supports the existing .attocode/rules/*.yaml format plus the
|
|
97
|
+
expanded format with explanation and examples fields.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
rules_dir: Directory containing \\*.yaml rule files.
|
|
101
|
+
source: RuleSource tag for loaded rules.
|
|
102
|
+
pack: Pack name to associate with loaded rules.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of parsed UnifiedRule instances. Invalid rules are skipped.
|
|
106
|
+
"""
|
|
107
|
+
rules_path = Path(rules_dir)
|
|
108
|
+
if not rules_path.is_dir():
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
import yaml # type: ignore[import-untyped]
|
|
113
|
+
except ImportError:
|
|
114
|
+
logger.debug("PyYAML not installed — skipping YAML rules from %s", rules_dir)
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
rules: list[UnifiedRule] = []
|
|
118
|
+
for yaml_file in sorted(rules_path.glob("*.yaml")):
|
|
119
|
+
try:
|
|
120
|
+
content = yaml_file.read_text(encoding="utf-8")
|
|
121
|
+
data = yaml.safe_load(content)
|
|
122
|
+
if data is None:
|
|
123
|
+
continue
|
|
124
|
+
items = [data] if isinstance(data, dict) else data if isinstance(data, list) else []
|
|
125
|
+
for i, item in enumerate(items):
|
|
126
|
+
try:
|
|
127
|
+
rule = _parse_yaml_rule(
|
|
128
|
+
item, source=source, pack=pack, origin=f"{yaml_file.name}[{i}]",
|
|
129
|
+
)
|
|
130
|
+
if rule is not None:
|
|
131
|
+
rules.append(rule)
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
logger.warning("Invalid rule in %s at index %d: %s", yaml_file.name, i, exc)
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
logger.warning("Failed to load rules from %s: %s", yaml_file, exc)
|
|
136
|
+
|
|
137
|
+
if rules:
|
|
138
|
+
logger.info("Loaded %d YAML rules from %s", len(rules), rules_dir)
|
|
139
|
+
return rules
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parse_yaml_rule(
|
|
143
|
+
data: dict,
|
|
144
|
+
*,
|
|
145
|
+
source: RuleSource = RuleSource.USER,
|
|
146
|
+
pack: str = "",
|
|
147
|
+
origin: str = "",
|
|
148
|
+
) -> UnifiedRule | None:
|
|
149
|
+
"""Parse a single YAML rule dict into a UnifiedRule."""
|
|
150
|
+
# ``pattern`` is required unless one of: ``patterns``/``pattern-either``
|
|
151
|
+
# provides a composite, OR ``structural_pattern`` provides a Tier-2
|
|
152
|
+
# ast-grep matcher (message-only structural rules are valid).
|
|
153
|
+
has_composite = "patterns" in data or "pattern-either" in data
|
|
154
|
+
has_structural = bool(data.get("structural_pattern"))
|
|
155
|
+
# Allow ``description`` as a synonym for ``message`` so structural-only
|
|
156
|
+
# rules don't have to repeat themselves.
|
|
157
|
+
if "message" not in data and "description" in data:
|
|
158
|
+
data = {**data, "message": data["description"]}
|
|
159
|
+
required = ["id", "message", "severity"]
|
|
160
|
+
if not (has_composite or has_structural):
|
|
161
|
+
required.append("pattern")
|
|
162
|
+
missing = [f for f in required if f not in data]
|
|
163
|
+
if missing:
|
|
164
|
+
logger.warning("Rule %s missing required fields: %s", origin, ", ".join(missing))
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
sev_str = str(data["severity"]).lower()
|
|
168
|
+
if sev_str not in _VALID_SEVERITIES:
|
|
169
|
+
logger.warning("Rule %s has invalid severity '%s'", origin, data["severity"])
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Category (default to security for backward compat)
|
|
173
|
+
cat_str = str(data.get("category", "security")).lower()
|
|
174
|
+
category = _YAML_CATEGORY_MAP.get(cat_str, RuleCategory.SECURITY)
|
|
175
|
+
|
|
176
|
+
# Build composite pattern if "patterns" or "pattern-either" is present
|
|
177
|
+
composite_pattern = None
|
|
178
|
+
if has_composite:
|
|
179
|
+
from attocode_intel.rules.combinators import build_composite_from_yaml
|
|
180
|
+
if "patterns" in data:
|
|
181
|
+
composite_src = data["patterns"] # list of dicts
|
|
182
|
+
else:
|
|
183
|
+
# Top-level pattern-either: wrap as dict for build_composite_from_yaml
|
|
184
|
+
composite_src = {"pattern-either": data["pattern-either"]}
|
|
185
|
+
composite_pattern = build_composite_from_yaml(composite_src)
|
|
186
|
+
if composite_pattern is None:
|
|
187
|
+
logger.warning("Rule %s has invalid composite patterns", origin)
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
# Compile primary pattern (may be absent for composite-only rules)
|
|
191
|
+
compiled: re.Pattern[str] | None = None
|
|
192
|
+
metavar_names: list[str] = []
|
|
193
|
+
if "pattern" in data:
|
|
194
|
+
pattern_str = str(data["pattern"])
|
|
195
|
+
try:
|
|
196
|
+
if has_metavars(pattern_str):
|
|
197
|
+
compiled, metavar_names = compile_metavar_pattern(pattern_str)
|
|
198
|
+
else:
|
|
199
|
+
compiled = re.compile(pattern_str)
|
|
200
|
+
except re.error as exc:
|
|
201
|
+
logger.warning("Rule %s has invalid regex: %s", origin, exc)
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
tier = RuleTier.STRUCTURAL if data.get("structural_pattern") else RuleTier.REGEX
|
|
205
|
+
|
|
206
|
+
# ast-grep's inline-rule YAML accepts a ``pattern`` object with either
|
|
207
|
+
# a bare string OR ``{context, selector}``. Specifying ``context``
|
|
208
|
+
# without ``selector`` produces an invalid rule that ast-grep rejects
|
|
209
|
+
# at parse time, which silently no-ops the rule. Catch it at load.
|
|
210
|
+
if data.get("structural_context") and not data.get("structural_selector"):
|
|
211
|
+
logger.warning(
|
|
212
|
+
"Rule %s sets structural_context without structural_selector; "
|
|
213
|
+
"ast-grep needs both — skipping this rule",
|
|
214
|
+
origin,
|
|
215
|
+
)
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
# Parse metavariable constraints
|
|
219
|
+
metavar_regex: dict[str, str] = {}
|
|
220
|
+
raw_mv_regex = data.get("metavariable-regex") or data.get("metavariable_regex")
|
|
221
|
+
if isinstance(raw_mv_regex, dict):
|
|
222
|
+
metavar_regex = {str(k): str(v) for k, v in raw_mv_regex.items()}
|
|
223
|
+
|
|
224
|
+
metavar_comparison: dict[str, str] = {}
|
|
225
|
+
raw_mv_cmp = data.get("metavariable-comparison") or data.get("metavariable_comparison")
|
|
226
|
+
if isinstance(raw_mv_cmp, dict):
|
|
227
|
+
metavar_comparison = {str(k): str(v) for k, v in raw_mv_cmp.items()}
|
|
228
|
+
|
|
229
|
+
fix = None
|
|
230
|
+
fix_data = data.get("fix")
|
|
231
|
+
if isinstance(fix_data, dict) and "search" in fix_data and "replace" in fix_data:
|
|
232
|
+
fix_search = str(fix_data["search"])
|
|
233
|
+
fix_replace = str(fix_data["replace"])
|
|
234
|
+
uses_metavars = has_metavars(fix_search) or has_metavars(fix_replace)
|
|
235
|
+
fix = AutoFix(search=fix_search, replace=fix_replace, uses_metavars=uses_metavars)
|
|
236
|
+
|
|
237
|
+
examples: list[FewShotExample] = []
|
|
238
|
+
for ex in data.get("examples", []):
|
|
239
|
+
if isinstance(ex, dict) and "bad" in ex and "good" in ex:
|
|
240
|
+
examples.append(FewShotExample(
|
|
241
|
+
bad_code=str(ex["bad"]),
|
|
242
|
+
good_code=str(ex["good"]),
|
|
243
|
+
explanation=str(ex.get("explanation", "")),
|
|
244
|
+
))
|
|
245
|
+
|
|
246
|
+
# Normalize languages and tags to lists (YAML allows string shorthand)
|
|
247
|
+
raw_langs = data.get("languages", [])
|
|
248
|
+
languages = [raw_langs] if isinstance(raw_langs, str) else list(raw_langs)
|
|
249
|
+
raw_tags = data.get("tags", [])
|
|
250
|
+
tags = [raw_tags] if isinstance(raw_tags, str) else list(raw_tags)
|
|
251
|
+
|
|
252
|
+
# External references (CWE links, blog posts, etc.) — preserved on import
|
|
253
|
+
raw_refs = data.get("references", [])
|
|
254
|
+
references = [raw_refs] if isinstance(raw_refs, str) else list(raw_refs)
|
|
255
|
+
|
|
256
|
+
return UnifiedRule(
|
|
257
|
+
id=str(data["id"]),
|
|
258
|
+
name=str(data.get("name", data["id"])),
|
|
259
|
+
description=str(data["message"]),
|
|
260
|
+
severity=RuleSeverity(sev_str),
|
|
261
|
+
category=category,
|
|
262
|
+
languages=languages,
|
|
263
|
+
pattern=compiled,
|
|
264
|
+
structural_pattern=str(data.get("structural_pattern", "")),
|
|
265
|
+
structural_context=str(data.get("structural_context", "")),
|
|
266
|
+
structural_selector=str(data.get("structural_selector", "")),
|
|
267
|
+
cwe=str(data.get("cwe", "")),
|
|
268
|
+
tags=tags,
|
|
269
|
+
source=source,
|
|
270
|
+
tier=tier,
|
|
271
|
+
confidence=float(data.get("confidence", 0.8)),
|
|
272
|
+
fix=fix,
|
|
273
|
+
enabled=bool(data.get("enabled", True)),
|
|
274
|
+
pack=pack,
|
|
275
|
+
explanation=str(data.get("explanation", "")),
|
|
276
|
+
examples=examples,
|
|
277
|
+
recommendation=str(data.get("recommendation", "")),
|
|
278
|
+
scan_comments=bool(data.get("scan_comments", False)),
|
|
279
|
+
metavars=metavar_names,
|
|
280
|
+
metavar_regex=metavar_regex,
|
|
281
|
+
metavar_comparison=metavar_comparison,
|
|
282
|
+
composite_pattern=composite_pattern,
|
|
283
|
+
references=references,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def load_user_rules(project_dir: str) -> list[UnifiedRule]:
|
|
288
|
+
"""Load user-defined rules from .attocode/rules/*.yaml."""
|
|
289
|
+
return load_yaml_rules(
|
|
290
|
+
Path(project_dir) / ".attocode" / "rules",
|
|
291
|
+
source=RuleSource.USER,
|
|
292
|
+
)
|