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,522 @@
|
|
|
1
|
+
"""Lightweight intra-procedural data flow analysis.
|
|
2
|
+
|
|
3
|
+
Tracks taint from sources (user input, request parameters) through
|
|
4
|
+
assignments and function calls to sinks (SQL execution, shell commands,
|
|
5
|
+
HTML output) within individual functions. Uses tree-sitter ASTs for
|
|
6
|
+
Python and JavaScript/TypeScript.
|
|
7
|
+
|
|
8
|
+
This is NOT a full compiler-grade taint tracker. It provides best-effort
|
|
9
|
+
detection of common vulnerability patterns (CWE-89, CWE-78, CWE-79,
|
|
10
|
+
CWE-22, CWE-918) without requiring compilation or type information.
|
|
11
|
+
|
|
12
|
+
Limitations:
|
|
13
|
+
- Intra-procedural only (does not follow function calls across files)
|
|
14
|
+
- No alias analysis (reassignment through containers not tracked)
|
|
15
|
+
- No type inference (relies on naming conventions and API patterns)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Source / Sink definitions
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True, frozen=True)
|
|
34
|
+
class TaintSource:
|
|
35
|
+
"""A source of tainted (user-controlled) data."""
|
|
36
|
+
name: str
|
|
37
|
+
pattern: re.Pattern[str]
|
|
38
|
+
language: str # "python", "javascript", or "" for all
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(slots=True, frozen=True)
|
|
42
|
+
class TaintSink:
|
|
43
|
+
"""A dangerous function that should not receive tainted data."""
|
|
44
|
+
name: str
|
|
45
|
+
pattern: re.Pattern[str]
|
|
46
|
+
cwe: str
|
|
47
|
+
message: str
|
|
48
|
+
language: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(slots=True)
|
|
52
|
+
class DataFlowFinding:
|
|
53
|
+
"""A taint flow from source to sink within a function."""
|
|
54
|
+
file_path: str
|
|
55
|
+
function_name: str
|
|
56
|
+
source_line: int
|
|
57
|
+
source_desc: str
|
|
58
|
+
sink_line: int
|
|
59
|
+
sink_desc: str
|
|
60
|
+
tainted_var: str
|
|
61
|
+
cwe: str
|
|
62
|
+
message: str
|
|
63
|
+
severity: str = "high"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(slots=True)
|
|
67
|
+
class DataFlowReport:
|
|
68
|
+
"""Results of data flow analysis."""
|
|
69
|
+
findings: list[DataFlowFinding]
|
|
70
|
+
functions_analyzed: int
|
|
71
|
+
files_analyzed: int
|
|
72
|
+
scan_time_ms: float = 0.0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Python sources and sinks
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
_PYTHON_SOURCES: list[TaintSource] = [
|
|
80
|
+
TaintSource("request_param", re.compile(
|
|
81
|
+
r"""\b(?:request\.(?:args|form|json|data|values|files|headers|cookies|GET|POST)|"""
|
|
82
|
+
r"""flask\.request\.\w+|"""
|
|
83
|
+
r"""self\.request\.\w+)"""
|
|
84
|
+
), "python"),
|
|
85
|
+
TaintSource("input_builtin", re.compile(r"""\binput\s*\("""), "python"),
|
|
86
|
+
TaintSource("sys_argv", re.compile(r"""\bsys\.argv\b"""), "python"),
|
|
87
|
+
TaintSource("environ_get", re.compile(r"""\bos\.environ\b"""), "python"),
|
|
88
|
+
TaintSource("query_param", re.compile(
|
|
89
|
+
r"""\b(?:params|query_params|request_data)\b"""
|
|
90
|
+
), "python"),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
_PYTHON_SINKS: list[TaintSink] = [
|
|
94
|
+
TaintSink("sql_execute", re.compile(
|
|
95
|
+
r"""\b(?:cursor\.execute|\.execute|\.executemany|\.raw)\s*\("""
|
|
96
|
+
), "CWE-89", "SQL injection: tainted data reaches SQL execution", "python"),
|
|
97
|
+
TaintSink("os_system", re.compile(
|
|
98
|
+
r"""\b(?:os\.system|os\.popen|subprocess\.(?:call|run|Popen|check_output|check_call))\s*\("""
|
|
99
|
+
), "CWE-78", "Command injection: tainted data reaches shell execution", "python"),
|
|
100
|
+
TaintSink("open_file", re.compile(
|
|
101
|
+
r"""\bopen\s*\("""
|
|
102
|
+
), "CWE-22", "Path traversal: tainted data used in file path", "python"),
|
|
103
|
+
TaintSink("url_request", re.compile(
|
|
104
|
+
r"""\b(?:requests\.(?:get|post|put|delete|patch|head)|urllib\.request\.urlopen|httpx\.(?:get|post))\s*\("""
|
|
105
|
+
), "CWE-918", "SSRF: tainted data used in URL construction", "python"),
|
|
106
|
+
TaintSink("html_output", re.compile(
|
|
107
|
+
r"""\b(?:render_template_string|Markup|mark_safe|SafeString)\s*\("""
|
|
108
|
+
), "CWE-79", "XSS: tainted data rendered as HTML without escaping", "python"),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# JavaScript/TypeScript sources and sinks
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
_JS_SOURCES: list[TaintSource] = [
|
|
116
|
+
TaintSource("req_param", re.compile(
|
|
117
|
+
r"""\b(?:req\.(?:body|params|query|headers|cookies)|"""
|
|
118
|
+
r"""request\.(?:body|params|query|headers))\b"""
|
|
119
|
+
), "javascript"),
|
|
120
|
+
TaintSource("url_search_params", re.compile(
|
|
121
|
+
r"""\b(?:URLSearchParams|location\.search|location\.hash|window\.location)\b"""
|
|
122
|
+
), "javascript"),
|
|
123
|
+
TaintSource("document_input", re.compile(
|
|
124
|
+
r"""\b(?:document\.getElementById|document\.querySelector|\.value)\b"""
|
|
125
|
+
), "javascript"),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
_JS_SINKS: list[TaintSink] = [
|
|
129
|
+
TaintSink("sql_query", re.compile(
|
|
130
|
+
r"""\b(?:\.query|\.execute|\.run)\s*\("""
|
|
131
|
+
), "CWE-89", "SQL injection: tainted data in database query", "javascript"),
|
|
132
|
+
TaintSink("exec_cmd", re.compile(
|
|
133
|
+
r"""\b(?:exec|execSync|spawn|spawnSync|execFile)\s*\("""
|
|
134
|
+
), "CWE-78", "Command injection: tainted data in shell command", "javascript"),
|
|
135
|
+
TaintSink("inner_html", re.compile(
|
|
136
|
+
r"""\.innerHTML\s*="""
|
|
137
|
+
), "CWE-79", "XSS: tainted data assigned to innerHTML", "javascript"),
|
|
138
|
+
TaintSink("redirect", re.compile(
|
|
139
|
+
r"""\b(?:res\.redirect|location\.href|window\.location)\s*="""
|
|
140
|
+
), "CWE-601", "Open redirect: tainted data in redirect URL", "javascript"),
|
|
141
|
+
TaintSink("fs_access", re.compile(
|
|
142
|
+
r"""\b(?:fs\.(?:readFile|writeFile|readFileSync|writeFileSync|createReadStream|unlink)|"""
|
|
143
|
+
r"""path\.(?:join|resolve))\s*\("""
|
|
144
|
+
), "CWE-22", "Path traversal: tainted data in file system path", "javascript"),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Taint analysis engine
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
_ASSIGNMENT_RE = re.compile(
|
|
153
|
+
r"""^\s*(?:(?:const|let|var)\s+)?(\w+)\s*=\s*(.+)$"""
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
_AUGMENTED_ASSIGN_RE = re.compile(
|
|
157
|
+
r"""^\s*(?:(?:const|let|var)\s+)?(\w+)\s*(?:\+=|\|=|\.append\(|\.extend\(|\.update\()(.+)"""
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
_FSTRING_VAR_RE = re.compile(r"""\{(\w+)""")
|
|
161
|
+
_FORMAT_VAR_RE = re.compile(r"""\.format\s*\([^)]*?(\w+)""")
|
|
162
|
+
_PERCENT_VAR_RE = re.compile(r"""%\s*(?:\((\w+)\)|(\w+))""")
|
|
163
|
+
_TEMPLATE_VAR_RE = re.compile(r"""\$\{(\w+)\}""")
|
|
164
|
+
_CONCAT_VAR_RE = re.compile(r"""\+\s*(\w+)""")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _extract_variables_from_expr(expr: str) -> set[str]:
|
|
168
|
+
"""Extract variable names referenced in an expression."""
|
|
169
|
+
variables: set[str] = set()
|
|
170
|
+
|
|
171
|
+
# f-string interpolation: f"...{var}..."
|
|
172
|
+
variables.update(_FSTRING_VAR_RE.findall(expr))
|
|
173
|
+
|
|
174
|
+
# .format() calls
|
|
175
|
+
variables.update(_FORMAT_VAR_RE.findall(expr))
|
|
176
|
+
|
|
177
|
+
# %-formatting
|
|
178
|
+
for groups in _PERCENT_VAR_RE.findall(expr):
|
|
179
|
+
variables.update(g for g in groups if g)
|
|
180
|
+
|
|
181
|
+
# Template literal interpolation: `...${var}...`
|
|
182
|
+
variables.update(_TEMPLATE_VAR_RE.findall(expr))
|
|
183
|
+
|
|
184
|
+
# String concatenation: "..." + var
|
|
185
|
+
variables.update(_CONCAT_VAR_RE.findall(expr))
|
|
186
|
+
|
|
187
|
+
# Direct variable reference (simple identifier on RHS)
|
|
188
|
+
tokens = re.findall(r'\b([a-zA-Z_]\w*)\b', expr)
|
|
189
|
+
variables.update(tokens)
|
|
190
|
+
|
|
191
|
+
# Filter out keywords and builtins
|
|
192
|
+
_KEYWORDS = frozenset({
|
|
193
|
+
"True", "False", "None", "true", "false", "null", "undefined",
|
|
194
|
+
"if", "else", "for", "while", "return", "import", "from",
|
|
195
|
+
"def", "class", "function", "const", "let", "var", "new",
|
|
196
|
+
"not", "and", "or", "in", "is", "as", "with", "try", "except",
|
|
197
|
+
"finally", "raise", "yield", "async", "await", "self", "cls",
|
|
198
|
+
"this", "super", "typeof", "instanceof",
|
|
199
|
+
})
|
|
200
|
+
return variables - _KEYWORDS
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def analyze_function_taint(
|
|
204
|
+
lines: list[str],
|
|
205
|
+
sources: list[TaintSource],
|
|
206
|
+
sinks: list[TaintSink],
|
|
207
|
+
function_name: str,
|
|
208
|
+
start_line: int,
|
|
209
|
+
) -> list[tuple[str, int, str, int, str, str, str]]:
|
|
210
|
+
"""Analyze a single function body for source-to-sink taint flows.
|
|
211
|
+
|
|
212
|
+
Returns list of (tainted_var, source_line, source_desc, sink_line, sink_desc, cwe, message).
|
|
213
|
+
"""
|
|
214
|
+
# Phase 1: Identify tainted variables (variables assigned from sources)
|
|
215
|
+
tainted: dict[str, tuple[int, str]] = {} # var_name → (line_no, source_desc)
|
|
216
|
+
|
|
217
|
+
for i, line in enumerate(lines):
|
|
218
|
+
line_no = start_line + i
|
|
219
|
+
stripped = line.strip()
|
|
220
|
+
if not stripped or stripped.startswith("#") or stripped.startswith("//"):
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
# Check if line contains a source
|
|
224
|
+
for source in sources:
|
|
225
|
+
if source.pattern.search(line):
|
|
226
|
+
# Find the variable being assigned from this source
|
|
227
|
+
m = _ASSIGNMENT_RE.match(line)
|
|
228
|
+
if m:
|
|
229
|
+
var_name = m.group(1)
|
|
230
|
+
tainted[var_name] = (line_no, source.name)
|
|
231
|
+
else:
|
|
232
|
+
# Source used directly (e.g., as function parameter)
|
|
233
|
+
# Track any variable on the left side of any assignment-like pattern
|
|
234
|
+
am = _AUGMENTED_ASSIGN_RE.match(line)
|
|
235
|
+
if am:
|
|
236
|
+
tainted[am.group(1)] = (line_no, source.name)
|
|
237
|
+
|
|
238
|
+
if not tainted:
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
# Phase 2: Propagate taint through assignments
|
|
242
|
+
# Simple forward propagation: if `b = f(a)` and `a` is tainted, `b` is tainted
|
|
243
|
+
changed = True
|
|
244
|
+
max_iterations = 10
|
|
245
|
+
iteration = 0
|
|
246
|
+
while changed and iteration < max_iterations:
|
|
247
|
+
changed = False
|
|
248
|
+
iteration += 1
|
|
249
|
+
for i, line in enumerate(lines):
|
|
250
|
+
m = _ASSIGNMENT_RE.match(line)
|
|
251
|
+
if not m:
|
|
252
|
+
continue
|
|
253
|
+
var_name = m.group(1)
|
|
254
|
+
if var_name in tainted:
|
|
255
|
+
continue # already tainted
|
|
256
|
+
rhs = m.group(2)
|
|
257
|
+
rhs_vars = _extract_variables_from_expr(rhs)
|
|
258
|
+
for rv in rhs_vars:
|
|
259
|
+
if rv in tainted:
|
|
260
|
+
tainted[var_name] = (start_line + i, f"propagated from {rv}")
|
|
261
|
+
changed = True
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
# Phase 3: Check if tainted variables reach sinks
|
|
265
|
+
findings: list[tuple[str, int, str, int, str, str, str]] = []
|
|
266
|
+
|
|
267
|
+
for i, line in enumerate(lines):
|
|
268
|
+
line_no = start_line + i
|
|
269
|
+
for sink in sinks:
|
|
270
|
+
if not sink.pattern.search(line):
|
|
271
|
+
continue
|
|
272
|
+
# Check if any tainted variable appears in this sink call
|
|
273
|
+
line_vars = _extract_variables_from_expr(line)
|
|
274
|
+
for var in line_vars:
|
|
275
|
+
if var in tainted:
|
|
276
|
+
source_line, source_desc = tainted[var]
|
|
277
|
+
findings.append((
|
|
278
|
+
var, source_line, source_desc,
|
|
279
|
+
line_no, sink.name, sink.cwe, sink.message,
|
|
280
|
+
))
|
|
281
|
+
break # one finding per sink line
|
|
282
|
+
|
|
283
|
+
return findings
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# File-level analysis
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
def _extract_function_bodies(content: str, language: str) -> list[tuple[str, int, int]]:
|
|
291
|
+
"""Extract function name, start line, end line from source content.
|
|
292
|
+
|
|
293
|
+
Uses simple regex-based extraction (not tree-sitter) for portability.
|
|
294
|
+
Returns list of (function_name, start_line, end_line) 1-indexed.
|
|
295
|
+
"""
|
|
296
|
+
functions: list[tuple[str, int, int]] = []
|
|
297
|
+
lines = content.splitlines()
|
|
298
|
+
|
|
299
|
+
if language == "python":
|
|
300
|
+
func_re = re.compile(r"""^(\s*)def\s+(\w+)\s*\(""")
|
|
301
|
+
in_func = False
|
|
302
|
+
func_name = ""
|
|
303
|
+
func_start = 0
|
|
304
|
+
func_indent = 0
|
|
305
|
+
|
|
306
|
+
for i, line in enumerate(lines, 1):
|
|
307
|
+
m = func_re.match(line)
|
|
308
|
+
if m:
|
|
309
|
+
if in_func:
|
|
310
|
+
functions.append((func_name, func_start, i - 1))
|
|
311
|
+
func_indent = len(m.group(1))
|
|
312
|
+
func_name = m.group(2)
|
|
313
|
+
func_start = i
|
|
314
|
+
in_func = True
|
|
315
|
+
elif in_func and line.strip() and not line.startswith(" " * (func_indent + 1)) and not line.strip().startswith("#"):
|
|
316
|
+
# Dedented line = end of function
|
|
317
|
+
if not line[0].isspace() or (len(line) - len(line.lstrip())) <= func_indent:
|
|
318
|
+
functions.append((func_name, func_start, i - 1))
|
|
319
|
+
in_func = False
|
|
320
|
+
|
|
321
|
+
if in_func:
|
|
322
|
+
functions.append((func_name, func_start, len(lines)))
|
|
323
|
+
|
|
324
|
+
elif language in ("javascript", "typescript"):
|
|
325
|
+
# Match: function name(...), const name = (...) =>, async function name(...)
|
|
326
|
+
func_re = re.compile(
|
|
327
|
+
r"""(?:(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>)"""
|
|
328
|
+
)
|
|
329
|
+
# Simple brace-counting for JS function boundaries
|
|
330
|
+
for i, line in enumerate(lines, 1):
|
|
331
|
+
m = func_re.search(line)
|
|
332
|
+
if m:
|
|
333
|
+
func_name = m.group(1) or m.group(2)
|
|
334
|
+
# Find the end by counting braces
|
|
335
|
+
brace_count = 0
|
|
336
|
+
started = False
|
|
337
|
+
end_line = i
|
|
338
|
+
for j in range(i - 1, len(lines)):
|
|
339
|
+
for ch in lines[j]:
|
|
340
|
+
if ch == "{":
|
|
341
|
+
brace_count += 1
|
|
342
|
+
started = True
|
|
343
|
+
elif ch == "}":
|
|
344
|
+
brace_count -= 1
|
|
345
|
+
if started and brace_count <= 0:
|
|
346
|
+
end_line = j + 1
|
|
347
|
+
break
|
|
348
|
+
else:
|
|
349
|
+
end_line = len(lines)
|
|
350
|
+
functions.append((func_name, i, end_line))
|
|
351
|
+
|
|
352
|
+
return functions
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def analyze_file(
|
|
356
|
+
file_path: str,
|
|
357
|
+
language: str = "",
|
|
358
|
+
) -> list[DataFlowFinding]:
|
|
359
|
+
"""Analyze a single file for data flow vulnerabilities.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
file_path: Absolute path to the source file.
|
|
363
|
+
language: Language hint (auto-detected from extension if empty).
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
List of DataFlowFinding instances.
|
|
367
|
+
"""
|
|
368
|
+
if not language:
|
|
369
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
370
|
+
lang_map = {
|
|
371
|
+
".py": "python", ".pyi": "python",
|
|
372
|
+
".js": "javascript", ".jsx": "javascript",
|
|
373
|
+
".ts": "typescript", ".tsx": "typescript",
|
|
374
|
+
".mjs": "javascript", ".cjs": "javascript",
|
|
375
|
+
}
|
|
376
|
+
language = lang_map.get(ext, "")
|
|
377
|
+
|
|
378
|
+
if language not in ("python", "javascript", "typescript"):
|
|
379
|
+
return []
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
content = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
383
|
+
except OSError:
|
|
384
|
+
return []
|
|
385
|
+
|
|
386
|
+
# Select sources and sinks for the language
|
|
387
|
+
if language == "python":
|
|
388
|
+
sources = _PYTHON_SOURCES
|
|
389
|
+
sinks = _PYTHON_SINKS
|
|
390
|
+
else:
|
|
391
|
+
sources = _JS_SOURCES
|
|
392
|
+
sinks = _JS_SINKS
|
|
393
|
+
|
|
394
|
+
# Extract functions and analyze each
|
|
395
|
+
functions = _extract_function_bodies(content, language)
|
|
396
|
+
lines = content.splitlines()
|
|
397
|
+
findings: list[DataFlowFinding] = []
|
|
398
|
+
|
|
399
|
+
for func_name, start, end in functions:
|
|
400
|
+
func_lines = lines[start - 1:end]
|
|
401
|
+
raw_findings = analyze_function_taint(
|
|
402
|
+
func_lines, sources, sinks, func_name, start,
|
|
403
|
+
)
|
|
404
|
+
for tvar, src_line, src_desc, snk_line, snk_desc, cwe, msg in raw_findings:
|
|
405
|
+
findings.append(DataFlowFinding(
|
|
406
|
+
file_path=file_path,
|
|
407
|
+
function_name=func_name,
|
|
408
|
+
source_line=src_line,
|
|
409
|
+
source_desc=src_desc,
|
|
410
|
+
sink_line=snk_line,
|
|
411
|
+
sink_desc=snk_desc,
|
|
412
|
+
tainted_var=tvar,
|
|
413
|
+
cwe=cwe,
|
|
414
|
+
message=msg,
|
|
415
|
+
))
|
|
416
|
+
|
|
417
|
+
return findings
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
# Project-level analysis
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
def analyze_project(
|
|
425
|
+
project_dir: str,
|
|
426
|
+
paths: list[str] | None = None,
|
|
427
|
+
) -> DataFlowReport:
|
|
428
|
+
"""Run data flow analysis across a project or specific files.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
project_dir: Project root directory.
|
|
432
|
+
paths: Specific file paths to analyze (relative to project root).
|
|
433
|
+
If None, scans all Python and JavaScript files.
|
|
434
|
+
"""
|
|
435
|
+
import time
|
|
436
|
+
|
|
437
|
+
start = time.monotonic()
|
|
438
|
+
findings: list[DataFlowFinding] = []
|
|
439
|
+
files_analyzed = 0
|
|
440
|
+
functions_analyzed = 0
|
|
441
|
+
|
|
442
|
+
_SKIP_DIRS = frozenset({
|
|
443
|
+
".git", "node_modules", "__pycache__", ".venv", "venv",
|
|
444
|
+
".tox", "dist", "build", ".next", ".nuxt",
|
|
445
|
+
})
|
|
446
|
+
_SCAN_EXTS = frozenset({".py", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"})
|
|
447
|
+
|
|
448
|
+
if paths:
|
|
449
|
+
file_list = [os.path.join(project_dir, p) for p in paths]
|
|
450
|
+
else:
|
|
451
|
+
file_list = []
|
|
452
|
+
for dirpath, dirnames, filenames in os.walk(project_dir):
|
|
453
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
454
|
+
for fname in filenames:
|
|
455
|
+
ext = os.path.splitext(fname)[1].lower()
|
|
456
|
+
if ext in _SCAN_EXTS:
|
|
457
|
+
file_list.append(os.path.join(dirpath, fname))
|
|
458
|
+
|
|
459
|
+
for abs_path in file_list:
|
|
460
|
+
if not os.path.isfile(abs_path):
|
|
461
|
+
continue
|
|
462
|
+
file_findings = analyze_file(abs_path)
|
|
463
|
+
# Fix relative paths in findings
|
|
464
|
+
for f in file_findings:
|
|
465
|
+
try:
|
|
466
|
+
f.file_path = os.path.relpath(abs_path, project_dir)
|
|
467
|
+
except ValueError:
|
|
468
|
+
f.file_path = abs_path
|
|
469
|
+
findings.extend(file_findings)
|
|
470
|
+
files_analyzed += 1
|
|
471
|
+
|
|
472
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
473
|
+
|
|
474
|
+
return DataFlowReport(
|
|
475
|
+
findings=findings,
|
|
476
|
+
functions_analyzed=functions_analyzed,
|
|
477
|
+
files_analyzed=files_analyzed,
|
|
478
|
+
scan_time_ms=round(elapsed, 1),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def format_report(report: DataFlowReport) -> str:
|
|
483
|
+
"""Format a DataFlowReport as human-readable text."""
|
|
484
|
+
lines: list[str] = []
|
|
485
|
+
|
|
486
|
+
lines.append("Data Flow Analysis Report")
|
|
487
|
+
lines.append(f"Files: {report.files_analyzed} | "
|
|
488
|
+
f"Findings: {len(report.findings)} | "
|
|
489
|
+
f"Time: {report.scan_time_ms:.0f}ms")
|
|
490
|
+
lines.append("")
|
|
491
|
+
|
|
492
|
+
if not report.findings:
|
|
493
|
+
lines.append("No data flow vulnerabilities detected.")
|
|
494
|
+
return "\n".join(lines)
|
|
495
|
+
|
|
496
|
+
# Group by CWE
|
|
497
|
+
by_cwe: dict[str, list[DataFlowFinding]] = {}
|
|
498
|
+
for f in report.findings:
|
|
499
|
+
by_cwe.setdefault(f.cwe, []).append(f)
|
|
500
|
+
|
|
501
|
+
cwe_labels = {
|
|
502
|
+
"CWE-89": "SQL Injection",
|
|
503
|
+
"CWE-78": "Command Injection",
|
|
504
|
+
"CWE-79": "Cross-Site Scripting (XSS)",
|
|
505
|
+
"CWE-22": "Path Traversal",
|
|
506
|
+
"CWE-918": "Server-Side Request Forgery (SSRF)",
|
|
507
|
+
"CWE-601": "Open Redirect",
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for cwe, cwe_findings in sorted(by_cwe.items()):
|
|
511
|
+
label = cwe_labels.get(cwe, cwe)
|
|
512
|
+
lines.append(f"## {label} [{cwe}] ({len(cwe_findings)} finding(s))")
|
|
513
|
+
for f in cwe_findings[:10]:
|
|
514
|
+
lines.append(f" {f.file_path}:{f.sink_line} in {f.function_name}()")
|
|
515
|
+
lines.append(f" Tainted variable '{f.tainted_var}' flows from "
|
|
516
|
+
f"line {f.source_line} ({f.source_desc}) to sink ({f.sink_desc})")
|
|
517
|
+
lines.append(f" {f.message}")
|
|
518
|
+
if len(cwe_findings) > 10:
|
|
519
|
+
lines.append(f" ... and {len(cwe_findings) - 10} more")
|
|
520
|
+
lines.append("")
|
|
521
|
+
|
|
522
|
+
return "\n".join(lines)
|