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,351 @@
|
|
|
1
|
+
"""Security compliance scanner.
|
|
2
|
+
|
|
3
|
+
Scans source files for secrets, anti-patterns, and dependency issues.
|
|
4
|
+
All scanning is local — no external API calls for basic mode.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from attocode_intel._internal.integrations.security.matcher import iter_pattern_matches
|
|
16
|
+
from attocode_intel._internal.integrations.security.patterns import (
|
|
17
|
+
ANTI_PATTERNS,
|
|
18
|
+
SECRET_PATTERNS,
|
|
19
|
+
Category,
|
|
20
|
+
SecurityPattern,
|
|
21
|
+
Severity,
|
|
22
|
+
)
|
|
23
|
+
from attocode_intel._internal.integrations.security.custom_rules import (
|
|
24
|
+
get_autofix_from_rules,
|
|
25
|
+
load_custom_rules,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Extensions worth scanning (source code, not binary/media)
|
|
31
|
+
_SCANNABLE_EXTENSIONS = {
|
|
32
|
+
".py", ".pyi", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
33
|
+
".rs", ".go", ".java", ".kt", ".rb", ".php", ".swift", ".cs",
|
|
34
|
+
".c", ".cpp", ".h", ".hpp", ".lua", ".sh", ".bash", ".zsh",
|
|
35
|
+
".yaml", ".yml", ".toml", ".json", ".env", ".cfg", ".ini", ".conf",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Dot-files that should still be scanned (e.g. .env contains secrets)
|
|
39
|
+
_SCANNABLE_DOTFILES = {".env", ".envrc", ".env.local", ".env.production", ".env.staging"}
|
|
40
|
+
_EXTRA_IGNORED_DIRS = {"site"}
|
|
41
|
+
_PATTERN_DEFINITION_FILES = {
|
|
42
|
+
os.path.normpath("src/attocode/integrations/security/patterns.py"),
|
|
43
|
+
os.path.normpath("tests/unit/code_intel/test_supply_chain_rules.py"),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class SecurityFinding:
|
|
49
|
+
"""A single security finding."""
|
|
50
|
+
|
|
51
|
+
severity: Severity
|
|
52
|
+
category: Category
|
|
53
|
+
file_path: str
|
|
54
|
+
line: int
|
|
55
|
+
message: str
|
|
56
|
+
recommendation: str
|
|
57
|
+
cwe_id: str = ""
|
|
58
|
+
pattern_name: str = ""
|
|
59
|
+
fix_diff: str = "" # unified diff suggestion for mechanical fixes
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Autofix templates: pattern_name -> (search, replace) for mechanical fixes.
|
|
63
|
+
# These are safe, deterministic transformations for common patterns.
|
|
64
|
+
_AUTOFIX_TEMPLATES: dict[str, tuple[str, str]] = {
|
|
65
|
+
"python_yaml_unsafe": ("yaml.load(", "yaml.safe_load("),
|
|
66
|
+
"python_shell_true": ("shell=True", "shell=False"),
|
|
67
|
+
"python_tempfile_insecure": ("tempfile.mktemp(", "tempfile.mkstemp("),
|
|
68
|
+
"python_verify_false": ("verify=False", "verify=True"),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(slots=True)
|
|
73
|
+
class SecurityReport:
|
|
74
|
+
"""Full security scan report."""
|
|
75
|
+
|
|
76
|
+
findings: list[SecurityFinding]
|
|
77
|
+
scan_time_ms: float
|
|
78
|
+
files_scanned: int
|
|
79
|
+
summary: dict[str, int] = field(default_factory=dict) # severity -> count
|
|
80
|
+
compliance_score: int = 100
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(slots=True)
|
|
84
|
+
class SecurityScanner:
|
|
85
|
+
"""Scan a project for security issues.
|
|
86
|
+
|
|
87
|
+
Three scan modes:
|
|
88
|
+
- ``quick``: secrets only, fast
|
|
89
|
+
- ``full``: secrets + anti-patterns + dependencies
|
|
90
|
+
- ``secrets``: only secret detection
|
|
91
|
+
- ``patterns``: only code anti-patterns
|
|
92
|
+
- ``dependencies``: only dependency audit
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
root_dir: str
|
|
96
|
+
_language_map: dict[str, str] = field(default_factory=dict, repr=False)
|
|
97
|
+
_custom_patterns: list[SecurityPattern] = field(default_factory=list, repr=False)
|
|
98
|
+
_custom_autofixes: dict[str, tuple[str, str]] = field(default_factory=dict, repr=False)
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
from attocode_intel._internal.integrations.context.codebase_context import EXTENSION_LANGUAGES
|
|
102
|
+
self._language_map = dict(EXTENSION_LANGUAGES)
|
|
103
|
+
self._custom_patterns = load_custom_rules(self.root_dir)
|
|
104
|
+
self._custom_autofixes = get_autofix_from_rules(self.root_dir)
|
|
105
|
+
|
|
106
|
+
def scan(
|
|
107
|
+
self,
|
|
108
|
+
mode: str = "full",
|
|
109
|
+
path: str = "",
|
|
110
|
+
) -> SecurityReport:
|
|
111
|
+
"""Run a security scan.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
mode: Scan mode — 'quick', 'full', 'secrets', 'patterns', 'dependencies'.
|
|
115
|
+
path: Optional subdirectory to scan (relative to root).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
SecurityReport with findings and compliance score.
|
|
119
|
+
"""
|
|
120
|
+
start = time.monotonic()
|
|
121
|
+
findings: list[SecurityFinding] = []
|
|
122
|
+
files_scanned = 0
|
|
123
|
+
|
|
124
|
+
# Resolve and validate path stays inside root_dir to prevent traversal
|
|
125
|
+
if path:
|
|
126
|
+
resolved = (Path(self.root_dir) / path).resolve()
|
|
127
|
+
root_resolved = Path(self.root_dir).resolve()
|
|
128
|
+
if not str(resolved).startswith(str(root_resolved) + os.sep) and resolved != root_resolved:
|
|
129
|
+
return SecurityReport(findings=[], scan_time_ms=0, files_scanned=0, summary={}, compliance_score=100)
|
|
130
|
+
scan_root = str(resolved)
|
|
131
|
+
else:
|
|
132
|
+
scan_root = self.root_dir
|
|
133
|
+
|
|
134
|
+
run_secrets = mode in ("quick", "full", "secrets")
|
|
135
|
+
run_patterns = mode in ("full", "patterns")
|
|
136
|
+
run_deps = mode in ("full", "dependencies")
|
|
137
|
+
|
|
138
|
+
if run_secrets or run_patterns:
|
|
139
|
+
files_scanned, file_findings = self._scan_files(
|
|
140
|
+
scan_root, run_secrets=run_secrets, run_patterns=run_patterns,
|
|
141
|
+
)
|
|
142
|
+
findings.extend(file_findings)
|
|
143
|
+
|
|
144
|
+
if run_deps:
|
|
145
|
+
from attocode_intel._internal.integrations.security.dependency_audit import DependencyAuditor
|
|
146
|
+
auditor = DependencyAuditor(root_dir=self.root_dir)
|
|
147
|
+
dep_findings = auditor.audit()
|
|
148
|
+
for df in dep_findings:
|
|
149
|
+
findings.append(SecurityFinding(
|
|
150
|
+
severity=df.severity,
|
|
151
|
+
category=df.category,
|
|
152
|
+
file_path=df.source_file,
|
|
153
|
+
line=0,
|
|
154
|
+
message=df.message,
|
|
155
|
+
recommendation=df.recommendation,
|
|
156
|
+
cwe_id=df.cwe_id,
|
|
157
|
+
pattern_name=df.package,
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
# Compute summary
|
|
161
|
+
summary: dict[str, int] = {}
|
|
162
|
+
for f in findings:
|
|
163
|
+
summary[f.severity] = summary.get(f.severity, 0) + 1
|
|
164
|
+
|
|
165
|
+
# Compute compliance score
|
|
166
|
+
score = self._compute_score(summary)
|
|
167
|
+
|
|
168
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
169
|
+
|
|
170
|
+
return SecurityReport(
|
|
171
|
+
findings=findings,
|
|
172
|
+
scan_time_ms=round(elapsed, 1),
|
|
173
|
+
files_scanned=files_scanned,
|
|
174
|
+
summary=summary,
|
|
175
|
+
compliance_score=score,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _scan_files(
|
|
179
|
+
self,
|
|
180
|
+
scan_root: str,
|
|
181
|
+
*,
|
|
182
|
+
run_secrets: bool = True,
|
|
183
|
+
run_patterns: bool = True,
|
|
184
|
+
) -> tuple[int, list[SecurityFinding]]:
|
|
185
|
+
"""Scan all source files for secrets and/or anti-patterns."""
|
|
186
|
+
findings: list[SecurityFinding] = []
|
|
187
|
+
files_scanned = 0
|
|
188
|
+
|
|
189
|
+
from attocode_intel._internal.integrations.context.codebase_context import (
|
|
190
|
+
DEFAULT_IGNORES,
|
|
191
|
+
SKIP_EXTENSIONS,
|
|
192
|
+
SKIP_FILENAMES,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
for dirpath, dirnames, filenames in os.walk(scan_root):
|
|
196
|
+
rel_dir = os.path.relpath(dirpath, self.root_dir)
|
|
197
|
+
rel_parts = set(Path(rel_dir).parts) if rel_dir not in (".", "") else set()
|
|
198
|
+
# Filter ignored directories
|
|
199
|
+
dirnames[:] = [
|
|
200
|
+
d for d in dirnames
|
|
201
|
+
if d not in DEFAULT_IGNORES
|
|
202
|
+
and d not in _EXTRA_IGNORED_DIRS
|
|
203
|
+
and d not in rel_parts
|
|
204
|
+
and not d.startswith(".")
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
for filename in filenames:
|
|
208
|
+
if filename.startswith(".") and filename not in _SCANNABLE_DOTFILES:
|
|
209
|
+
continue
|
|
210
|
+
if filename in SKIP_FILENAMES:
|
|
211
|
+
continue
|
|
212
|
+
ext = Path(filename).suffix.lower()
|
|
213
|
+
if ext in SKIP_EXTENSIONS:
|
|
214
|
+
continue
|
|
215
|
+
if ext not in _SCANNABLE_EXTENSIONS:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
full_path = os.path.join(dirpath, filename)
|
|
219
|
+
try:
|
|
220
|
+
rel_path = os.path.relpath(full_path, self.root_dir)
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
rel_path_norm = os.path.normpath(rel_path)
|
|
224
|
+
|
|
225
|
+
if rel_path_norm in _PATTERN_DEFINITION_FILES:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
content = Path(full_path).read_text(
|
|
230
|
+
encoding="utf-8", errors="replace",
|
|
231
|
+
)
|
|
232
|
+
except OSError:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
files_scanned += 1
|
|
236
|
+
language = self._language_map.get(ext, "")
|
|
237
|
+
|
|
238
|
+
if run_secrets:
|
|
239
|
+
findings.extend(
|
|
240
|
+
self._scan_content(content, rel_path, SECRET_PATTERNS, language),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if run_patterns:
|
|
244
|
+
findings.extend(
|
|
245
|
+
self._scan_content(content, rel_path, ANTI_PATTERNS, language),
|
|
246
|
+
)
|
|
247
|
+
if self._custom_patterns:
|
|
248
|
+
findings.extend(
|
|
249
|
+
self._scan_content(content, rel_path, self._custom_patterns, language),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return files_scanned, findings
|
|
253
|
+
|
|
254
|
+
def _scan_content(
|
|
255
|
+
self,
|
|
256
|
+
content: str,
|
|
257
|
+
file_path: str,
|
|
258
|
+
patterns: list[SecurityPattern],
|
|
259
|
+
language: str,
|
|
260
|
+
) -> list[SecurityFinding]:
|
|
261
|
+
"""Scan file content against a set of patterns."""
|
|
262
|
+
findings: list[SecurityFinding] = []
|
|
263
|
+
for line_no, line_text, pat in iter_pattern_matches(content, patterns, language):
|
|
264
|
+
fix_diff = ""
|
|
265
|
+
template = _AUTOFIX_TEMPLATES.get(pat.name) or self._custom_autofixes.get(pat.name)
|
|
266
|
+
if template and template[0] in line_text:
|
|
267
|
+
old_line = line_text
|
|
268
|
+
new_line = line_text.replace(template[0], template[1], 1)
|
|
269
|
+
fix_diff = (
|
|
270
|
+
f"--- a/{file_path}\n"
|
|
271
|
+
f"+++ b/{file_path}\n"
|
|
272
|
+
f"@@ -{line_no},1 +{line_no},1 @@\n"
|
|
273
|
+
f"-{old_line}\n"
|
|
274
|
+
f"+{new_line}"
|
|
275
|
+
)
|
|
276
|
+
findings.append(SecurityFinding(
|
|
277
|
+
severity=pat.severity,
|
|
278
|
+
category=pat.category,
|
|
279
|
+
file_path=file_path,
|
|
280
|
+
line=line_no,
|
|
281
|
+
message=pat.message,
|
|
282
|
+
recommendation=pat.recommendation,
|
|
283
|
+
cwe_id=pat.cwe_id,
|
|
284
|
+
pattern_name=pat.name,
|
|
285
|
+
fix_diff=fix_diff,
|
|
286
|
+
))
|
|
287
|
+
return findings
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def _compute_score(summary: dict[str, int]) -> int:
|
|
291
|
+
"""Compute compliance score (0-100).
|
|
292
|
+
|
|
293
|
+
score = 100 - (critical * 20 + high * 10 + medium * 3 + low * 1)
|
|
294
|
+
"""
|
|
295
|
+
deductions = (
|
|
296
|
+
summary.get("critical", 0) * 20
|
|
297
|
+
+ summary.get("high", 0) * 10
|
|
298
|
+
+ summary.get("medium", 0) * 3
|
|
299
|
+
+ summary.get("low", 0) * 1
|
|
300
|
+
)
|
|
301
|
+
return max(0, min(100, 100 - deductions))
|
|
302
|
+
|
|
303
|
+
def format_report(self, report: SecurityReport) -> str:
|
|
304
|
+
"""Format a SecurityReport as human-readable text."""
|
|
305
|
+
lines: list[str] = []
|
|
306
|
+
|
|
307
|
+
# Header
|
|
308
|
+
lines.append("Security Scan Report")
|
|
309
|
+
lines.append(f"Score: {report.compliance_score}/100 | "
|
|
310
|
+
f"Files: {report.files_scanned} | "
|
|
311
|
+
f"Time: {report.scan_time_ms:.0f}ms")
|
|
312
|
+
lines.append("")
|
|
313
|
+
|
|
314
|
+
# Summary
|
|
315
|
+
if report.summary:
|
|
316
|
+
parts = []
|
|
317
|
+
for sev in ("critical", "high", "medium", "low", "info"):
|
|
318
|
+
count = report.summary.get(sev, 0)
|
|
319
|
+
if count > 0:
|
|
320
|
+
parts.append(f"{sev}: {count}")
|
|
321
|
+
lines.append(f"Findings: {' | '.join(parts)}")
|
|
322
|
+
lines.append("")
|
|
323
|
+
|
|
324
|
+
if not report.findings:
|
|
325
|
+
lines.append("No security issues found.")
|
|
326
|
+
return "\n".join(lines)
|
|
327
|
+
|
|
328
|
+
# Group findings by severity
|
|
329
|
+
by_severity: dict[str, list[SecurityFinding]] = {}
|
|
330
|
+
for f in report.findings:
|
|
331
|
+
by_severity.setdefault(f.severity, []).append(f)
|
|
332
|
+
|
|
333
|
+
for sev in ("critical", "high", "medium", "low", "info"):
|
|
334
|
+
group = by_severity.get(sev, [])
|
|
335
|
+
if not group:
|
|
336
|
+
continue
|
|
337
|
+
lines.append(f"## {sev.upper()} ({len(group)})")
|
|
338
|
+
for f in group[:20]: # Cap per-severity to avoid huge output
|
|
339
|
+
cwe = f" [{f.cwe_id}]" if f.cwe_id else ""
|
|
340
|
+
lines.append(f" {f.file_path}:{f.line}{cwe}")
|
|
341
|
+
lines.append(f" {f.message}")
|
|
342
|
+
lines.append(f" → {f.recommendation}")
|
|
343
|
+
if f.fix_diff:
|
|
344
|
+
lines.append(f" Autofix:")
|
|
345
|
+
for dl in f.fix_diff.splitlines():
|
|
346
|
+
lines.append(f" {dl}")
|
|
347
|
+
if len(group) > 20:
|
|
348
|
+
lines.append(f" ... and {len(group) - 20} more {sev} findings")
|
|
349
|
+
lines.append("")
|
|
350
|
+
|
|
351
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal code intelligence support; not a public API."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Gitignore-compatible path filtering using pathspec."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pathspec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IgnoreManager:
|
|
11
|
+
"""Manages .gitignore-style path filtering.
|
|
12
|
+
|
|
13
|
+
Loads patterns from .gitignore files and provides matching.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, root: str | Path = ".") -> None:
|
|
17
|
+
self._root = Path(root)
|
|
18
|
+
self._spec: pathspec.PathSpec | None = None
|
|
19
|
+
self._load()
|
|
20
|
+
|
|
21
|
+
def _load(self) -> None:
|
|
22
|
+
"""Load .gitignore patterns from the root directory."""
|
|
23
|
+
patterns: list[str] = []
|
|
24
|
+
|
|
25
|
+
# Always ignore these
|
|
26
|
+
patterns.extend([
|
|
27
|
+
".git/",
|
|
28
|
+
"node_modules/",
|
|
29
|
+
"__pycache__/",
|
|
30
|
+
".venv/",
|
|
31
|
+
"*.pyc",
|
|
32
|
+
".DS_Store",
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
# Load project .gitignore
|
|
36
|
+
gitignore = self._root / ".gitignore"
|
|
37
|
+
if gitignore.is_file():
|
|
38
|
+
try:
|
|
39
|
+
text = gitignore.read_text(encoding="utf-8", errors="replace")
|
|
40
|
+
for line in text.splitlines():
|
|
41
|
+
line = line.strip()
|
|
42
|
+
if line and not line.startswith("#"):
|
|
43
|
+
patterns.append(line)
|
|
44
|
+
except OSError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
self._spec = pathspec.PathSpec.from_lines("gitignore", patterns)
|
|
48
|
+
|
|
49
|
+
def is_ignored(self, path: str | Path) -> bool:
|
|
50
|
+
"""Check if a path should be ignored."""
|
|
51
|
+
if self._spec is None:
|
|
52
|
+
return False
|
|
53
|
+
rel = str(path)
|
|
54
|
+
# pathspec expects forward slashes
|
|
55
|
+
rel = rel.replace("\\", "/")
|
|
56
|
+
return self._spec.match_file(rel)
|
|
57
|
+
|
|
58
|
+
def filter_paths(self, paths: list[str]) -> list[str]:
|
|
59
|
+
"""Filter a list of paths, returning only non-ignored ones."""
|
|
60
|
+
return [p for p in paths if not self.is_ignored(p)]
|
|
61
|
+
|
|
62
|
+
def reload(self) -> None:
|
|
63
|
+
"""Reload patterns from disk."""
|
|
64
|
+
self._load()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Token estimation using tiktoken."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tiktoken
|
|
6
|
+
|
|
7
|
+
# Default encoding for Claude / GPT-4 class models
|
|
8
|
+
_DEFAULT_ENCODING = "cl100k_base"
|
|
9
|
+
_encoder: tiktoken.Encoding | None = None
|
|
10
|
+
|
|
11
|
+
# Shared ratio for quick estimation without tiktoken
|
|
12
|
+
CHARS_PER_TOKEN = 3.5
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_encoder() -> tiktoken.Encoding:
|
|
16
|
+
global _encoder
|
|
17
|
+
if _encoder is None:
|
|
18
|
+
_encoder = tiktoken.get_encoding(_DEFAULT_ENCODING)
|
|
19
|
+
return _encoder
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def count_tokens(text: str) -> int:
|
|
23
|
+
"""Count tokens in text using tiktoken.
|
|
24
|
+
|
|
25
|
+
Falls back to character-based estimation if tiktoken fails.
|
|
26
|
+
"""
|
|
27
|
+
if not text:
|
|
28
|
+
return 0
|
|
29
|
+
try:
|
|
30
|
+
return len(_get_encoder().encode(text))
|
|
31
|
+
except Exception:
|
|
32
|
+
return estimate_tokens(text)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def estimate_tokens(text: str) -> int:
|
|
36
|
+
"""Quick character-based token estimation without tiktoken."""
|
|
37
|
+
if not text:
|
|
38
|
+
return 0
|
|
39
|
+
return max(1, int(len(text) / CHARS_PER_TOKEN))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def estimate_messages_tokens(messages: list[dict[str, str]]) -> int:
|
|
43
|
+
"""Estimate total tokens for a list of message dicts."""
|
|
44
|
+
total = 0
|
|
45
|
+
for msg in messages:
|
|
46
|
+
# Each message has ~4 token overhead for role/formatting
|
|
47
|
+
total += 4
|
|
48
|
+
content = msg.get("content", "")
|
|
49
|
+
if content:
|
|
50
|
+
total += count_tokens(content)
|
|
51
|
+
return total
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal code intelligence support; not a public API."""
|