patchi 0.7.5__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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- patchi-0.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DependencyScanner — package files, CVE status via OSV API.
|
|
3
|
+
|
|
4
|
+
Scans dependency files including:
|
|
5
|
+
- package.json (npm/yarn)
|
|
6
|
+
- requirements.txt (pip)
|
|
7
|
+
- Pipfile (pipenv)
|
|
8
|
+
- poetry.lock (Poetry)
|
|
9
|
+
- Cargo.toml (Cargo/Rust)
|
|
10
|
+
- go.mod (Go modules)
|
|
11
|
+
- composer.json (Composer/PHP)
|
|
12
|
+
- Gemfile (Bundler/Ruby)
|
|
13
|
+
|
|
14
|
+
Checks versions against OSV (Open Source Vulnerability) database
|
|
15
|
+
for known CVEs and security issues.
|
|
16
|
+
|
|
17
|
+
Does NOT call AI.
|
|
18
|
+
Does NOT write to disk.
|
|
19
|
+
Does NOT touch the queue.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.request
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
|
|
31
|
+
from patchi.core.constants import is_offline
|
|
32
|
+
|
|
33
|
+
from .base import (
|
|
34
|
+
AgentGroup,
|
|
35
|
+
AgentInput,
|
|
36
|
+
AgentResult,
|
|
37
|
+
AgentStatus,
|
|
38
|
+
BaseAgent,
|
|
39
|
+
Finding,
|
|
40
|
+
Severity,
|
|
41
|
+
make_finding,
|
|
42
|
+
register,
|
|
43
|
+
safe_rglob,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Keep a direct alias to the original make_finding to avoid accidental
|
|
47
|
+
# recursive replacement when updating calls to use the compatibility wrapper.
|
|
48
|
+
_base_make_finding = make_finding
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_log = logging.getLogger("patchi.agents.dependency_scanner")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@register
|
|
55
|
+
class DependencyScanner(BaseAgent):
|
|
56
|
+
"""Scanner for project dependencies and CVEs."""
|
|
57
|
+
|
|
58
|
+
group = AgentGroup.SCANNER
|
|
59
|
+
name = "DependencyScanner"
|
|
60
|
+
description = "Dependency files, CVE status via OSV API"
|
|
61
|
+
|
|
62
|
+
def _mkf(self, *args, **kwargs) -> Finding:
|
|
63
|
+
"""Compatibility wrapper for legacy make_finding calls in this scanner."""
|
|
64
|
+
# Old positional style: (Severity, file, line_start, title, description, evidence)
|
|
65
|
+
if args and isinstance(args[0], Severity):
|
|
66
|
+
severity = args[0]
|
|
67
|
+
file = args[1] if len(args) > 1 else ""
|
|
68
|
+
line = args[2] if len(args) > 2 else 0
|
|
69
|
+
title = args[3] if len(args) > 3 else ""
|
|
70
|
+
message = args[4] if len(args) > 4 else ""
|
|
71
|
+
evidence = args[5] if len(args) > 5 else ""
|
|
72
|
+
finding_type = kwargs.pop("finding_type", None) or title.lower().replace(
|
|
73
|
+
" ", "_"
|
|
74
|
+
).replace(":", "").replace("'", "").replace("-", "_")
|
|
75
|
+
return _base_make_finding(
|
|
76
|
+
self.name,
|
|
77
|
+
finding_type,
|
|
78
|
+
severity,
|
|
79
|
+
file,
|
|
80
|
+
message,
|
|
81
|
+
line=line,
|
|
82
|
+
evidence=evidence,
|
|
83
|
+
**kwargs,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Keyword style: severity=..., file=..., title=..., description=..., evidence=...
|
|
87
|
+
if "severity" in kwargs and ("file" in kwargs or "file_path" in kwargs):
|
|
88
|
+
severity = kwargs.pop("severity")
|
|
89
|
+
file = kwargs.pop("file", kwargs.pop("file_path", ""))
|
|
90
|
+
line = kwargs.pop("line_start", kwargs.pop("line", 0))
|
|
91
|
+
title = kwargs.pop("title", "")
|
|
92
|
+
message = kwargs.pop("description", title)
|
|
93
|
+
evidence = kwargs.pop("evidence", "")
|
|
94
|
+
finding_type = kwargs.pop("finding_type", None) or title.lower().replace(
|
|
95
|
+
" ", "_"
|
|
96
|
+
).replace(":", "").replace("'", "").replace("-", "_")
|
|
97
|
+
return _base_make_finding(
|
|
98
|
+
self.name,
|
|
99
|
+
finding_type,
|
|
100
|
+
severity,
|
|
101
|
+
file,
|
|
102
|
+
message,
|
|
103
|
+
line=line,
|
|
104
|
+
evidence=evidence,
|
|
105
|
+
**kwargs,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return _base_make_finding(*args, **kwargs)
|
|
109
|
+
|
|
110
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
111
|
+
"""Scan dependency files for vulnerabilities.
|
|
112
|
+
|
|
113
|
+
Populates `result.data['dependencies']` and `result.data['total_deps']`.
|
|
114
|
+
"""
|
|
115
|
+
findings: list[Finding] = []
|
|
116
|
+
dependencies: dict[str, str] = {}
|
|
117
|
+
vulns_count = 0
|
|
118
|
+
|
|
119
|
+
# Define dependency file patterns
|
|
120
|
+
dep_files = [
|
|
121
|
+
"package.json",
|
|
122
|
+
"requirements.txt",
|
|
123
|
+
"Pipfile",
|
|
124
|
+
"poetry.lock",
|
|
125
|
+
"Cargo.toml",
|
|
126
|
+
"Cargo.lock",
|
|
127
|
+
"go.mod",
|
|
128
|
+
"go.sum",
|
|
129
|
+
"composer.json",
|
|
130
|
+
"Gemfile",
|
|
131
|
+
"Gemfile.lock",
|
|
132
|
+
"yarn.lock",
|
|
133
|
+
"pnpm-lock.yaml",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
# Search for dependency files and build dependency map
|
|
137
|
+
found_any = False
|
|
138
|
+
for dep_file in dep_files:
|
|
139
|
+
for file_path in safe_rglob(inp.root, dep_file):
|
|
140
|
+
if file_path.is_file():
|
|
141
|
+
rel_path = file_path.relative_to(inp.root).as_posix()
|
|
142
|
+
if not self._should_skip_file(rel_path, inp):
|
|
143
|
+
found_any = True
|
|
144
|
+
file_findings = self._scan_dep_file(file_path, rel_path)
|
|
145
|
+
findings.extend(file_findings)
|
|
146
|
+
|
|
147
|
+
# Lightweight parsing to populate dependencies dict for tests
|
|
148
|
+
try:
|
|
149
|
+
content = file_path.read_text(encoding="utf-8")
|
|
150
|
+
if file_path.name == "package.json":
|
|
151
|
+
data = json.loads(content)
|
|
152
|
+
deps = data.get("dependencies", {})
|
|
153
|
+
dev_deps = data.get("devDependencies", {})
|
|
154
|
+
for n, v in {**deps, **dev_deps}.items():
|
|
155
|
+
dependencies[n] = v
|
|
156
|
+
elif file_path.name == "requirements.txt":
|
|
157
|
+
for line in content.splitlines():
|
|
158
|
+
ln = line.strip()
|
|
159
|
+
if not ln or ln.startswith("#"):
|
|
160
|
+
continue
|
|
161
|
+
pkg_name = ln
|
|
162
|
+
version = "unknown"
|
|
163
|
+
for sep in ["==", ">=", "<=", ">", "<"]:
|
|
164
|
+
if sep in ln:
|
|
165
|
+
pkg_name, version = ln.split(sep, 1)
|
|
166
|
+
break
|
|
167
|
+
dependencies[pkg_name.strip()] = version.strip()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
_log.warning("DependencyScanner._run failed: %s", e)
|
|
170
|
+
|
|
171
|
+
# If no dependency files were found, emit an informational finding
|
|
172
|
+
if not found_any:
|
|
173
|
+
result.add_finding(
|
|
174
|
+
self._mkf(
|
|
175
|
+
severity=Severity.INFO,
|
|
176
|
+
file="",
|
|
177
|
+
line_start=0,
|
|
178
|
+
title="No dependency files found",
|
|
179
|
+
description="Project contains no recognized dependency files",
|
|
180
|
+
finding_type="no_dependency_file",
|
|
181
|
+
evidence="",
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Aggregate dependencies and vulnerability counts from findings
|
|
186
|
+
for f in findings:
|
|
187
|
+
# Titles created by legacy code use formats like "Dependency: name@version" or "VULNERABILITY: name@version"
|
|
188
|
+
getattr(f, "message", "") or getattr(f, "type", "") or ""
|
|
189
|
+
# Prefer finding.type if it encodes vulnerability info
|
|
190
|
+
if f.message.startswith("VULNERABILITY:") or f.type.startswith("vulnerability"):
|
|
191
|
+
vulns_count += 1
|
|
192
|
+
if f.message.startswith("Dependency:"):
|
|
193
|
+
# message may be like "Dependency: name@version" if compat preserved
|
|
194
|
+
dep_text = f.message[len("Dependency:") :].strip()
|
|
195
|
+
if "@" in dep_text:
|
|
196
|
+
name, version = dep_text.split("@", 1)
|
|
197
|
+
else:
|
|
198
|
+
name, version = dep_text, "unknown"
|
|
199
|
+
dependencies[name.strip()] = version.strip()
|
|
200
|
+
|
|
201
|
+
# Add findings to result
|
|
202
|
+
for f in findings:
|
|
203
|
+
result.add_finding(f)
|
|
204
|
+
|
|
205
|
+
result.data["dependencies"] = dependencies
|
|
206
|
+
result.data["total_deps"] = len(dependencies)
|
|
207
|
+
result.data["vulnerable_deps"] = vulns_count
|
|
208
|
+
result.files_scanned = sum(1 for _ in safe_rglob(inp.root, "*"))
|
|
209
|
+
result.data["needs_ai"] = False
|
|
210
|
+
if result.status == AgentStatus.RUNNING:
|
|
211
|
+
result.status = AgentStatus.DONE
|
|
212
|
+
|
|
213
|
+
def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
|
|
214
|
+
"""Check if file should be skipped based on restrictions."""
|
|
215
|
+
# Skip ignored directories (node_modules, .venv, etc.)
|
|
216
|
+
from pathlib import PurePosixPath
|
|
217
|
+
|
|
218
|
+
if any(p in DEFAULT_IGNORE_DIRS for p in PurePosixPath(file_path).parts):
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
from pathlib import PurePosixPath
|
|
222
|
+
|
|
223
|
+
# Check restrictions
|
|
224
|
+
restrictions = inp.config.get("restrictions", [])
|
|
225
|
+
for r in restrictions:
|
|
226
|
+
if r.get("enabled", True):
|
|
227
|
+
path = r["path"]
|
|
228
|
+
if file_path.startswith(path) or PurePosixPath(file_path).match(path):
|
|
229
|
+
if r["type"] == "NO_TOUCH":
|
|
230
|
+
return True
|
|
231
|
+
elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
|
|
232
|
+
return True
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def _scan_dep_file(self, file_path: Path, rel_path: str) -> list[Finding]:
|
|
236
|
+
"""Scan a dependency file."""
|
|
237
|
+
findings = []
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
content = file_path.read_text(encoding="utf-8")
|
|
241
|
+
|
|
242
|
+
if file_path.name == "package.json":
|
|
243
|
+
findings.extend(self._scan_package_json(file_path, rel_path, content))
|
|
244
|
+
elif file_path.name == "requirements.txt":
|
|
245
|
+
findings.extend(self._scan_requirements_txt(file_path, rel_path, content))
|
|
246
|
+
elif file_path.name == "Pipfile":
|
|
247
|
+
findings.extend(self._scan_pipfile(file_path, rel_path, content))
|
|
248
|
+
elif file_path.name == "poetry.lock":
|
|
249
|
+
findings.extend(self._scan_poetry_lock(file_path, rel_path, content))
|
|
250
|
+
elif file_path.name in ["Cargo.toml", "Cargo.lock"]:
|
|
251
|
+
findings.extend(self._scan_cargo_file(file_path, rel_path, content))
|
|
252
|
+
elif file_path.name == "go.mod":
|
|
253
|
+
findings.extend(self._scan_go_mod(file_path, rel_path, content))
|
|
254
|
+
elif file_path.name == "composer.json":
|
|
255
|
+
findings.extend(self._scan_composer_json(file_path, rel_path, content))
|
|
256
|
+
elif file_path.name in ["Gemfile", "Gemfile.lock"]:
|
|
257
|
+
findings.extend(self._scan_gemfile(file_path, rel_path, content))
|
|
258
|
+
elif file_path.name in ["yarn.lock", "pnpm-lock.yaml"]:
|
|
259
|
+
findings.extend(self._scan_lock_file(file_path, rel_path, content))
|
|
260
|
+
|
|
261
|
+
findings.append(
|
|
262
|
+
self._mkf(
|
|
263
|
+
severity=Severity.INFO,
|
|
264
|
+
file=rel_path,
|
|
265
|
+
line_start=0,
|
|
266
|
+
title=f"Dependency File: {file_path.name}",
|
|
267
|
+
description="Found dependency file with vulnerability checking",
|
|
268
|
+
evidence=f"File: {file_path.name}",
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
findings.append(
|
|
274
|
+
self._mkf(
|
|
275
|
+
severity=Severity.LOW,
|
|
276
|
+
file=rel_path,
|
|
277
|
+
line_start=0,
|
|
278
|
+
title="Dependency file read error",
|
|
279
|
+
description=f"Could not read dependency file {file_path.name}: {str(e)}",
|
|
280
|
+
evidence=str(e),
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return findings
|
|
285
|
+
|
|
286
|
+
def _scan_package_json(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
287
|
+
"""Scan package.json for dependencies and vulnerabilities."""
|
|
288
|
+
findings = []
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
data = json.loads(content)
|
|
292
|
+
deps = data.get("dependencies", {})
|
|
293
|
+
dev_deps = data.get("devDependencies", {})
|
|
294
|
+
|
|
295
|
+
# Combine all dependencies
|
|
296
|
+
all_deps = {}
|
|
297
|
+
all_deps.update(dict(deps.items()))
|
|
298
|
+
all_deps.update(dict(dev_deps.items()))
|
|
299
|
+
|
|
300
|
+
for name, version in all_deps.items():
|
|
301
|
+
findings.append(
|
|
302
|
+
self._mkf(
|
|
303
|
+
severity=Severity.INFO,
|
|
304
|
+
file=rel_path,
|
|
305
|
+
line_start=0,
|
|
306
|
+
title=f"Dependency: {name}@{version}",
|
|
307
|
+
description="JavaScript dependency",
|
|
308
|
+
evidence=f"Version: {version}",
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Check for vulnerabilities via OSV API
|
|
313
|
+
vulns = self._check_npm_vulnerabilities(name, version)
|
|
314
|
+
for vuln in vulns:
|
|
315
|
+
severity = self._map_osv_severity(
|
|
316
|
+
vuln.get("severity", [{}])[0].get("score", "UNKNOWN")
|
|
317
|
+
)
|
|
318
|
+
findings.append(
|
|
319
|
+
self._mkf(
|
|
320
|
+
severity=severity,
|
|
321
|
+
file=rel_path,
|
|
322
|
+
line_start=0,
|
|
323
|
+
title=f"VULNERABILITY: {name}@{version}",
|
|
324
|
+
description=vuln.get("summary", "Unknown vulnerability"),
|
|
325
|
+
evidence=json.dumps(vuln),
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
except json.JSONDecodeError as e:
|
|
330
|
+
findings.append(
|
|
331
|
+
self._mkf(
|
|
332
|
+
severity=Severity.HIGH,
|
|
333
|
+
file=rel_path,
|
|
334
|
+
line_start=e.lineno,
|
|
335
|
+
title="Invalid package.json",
|
|
336
|
+
description=f"JSON syntax error: {str(e)}",
|
|
337
|
+
evidence=e.msg,
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return findings
|
|
342
|
+
|
|
343
|
+
def _scan_requirements_txt(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
344
|
+
"""Scan requirements.txt for dependencies."""
|
|
345
|
+
findings = []
|
|
346
|
+
|
|
347
|
+
lines = content.splitlines()
|
|
348
|
+
for i, line in enumerate(lines, 1):
|
|
349
|
+
line = line.strip()
|
|
350
|
+
if line and not line.startswith("#") and not line.startswith("-"):
|
|
351
|
+
# Extract package name and version
|
|
352
|
+
if "==" in line:
|
|
353
|
+
pkg_name, version = line.split("==", 1)
|
|
354
|
+
elif ">=" in line:
|
|
355
|
+
pkg_name, version = line.split(">=", 1)
|
|
356
|
+
elif "<=" in line:
|
|
357
|
+
pkg_name, version = line.split("<=", 1)
|
|
358
|
+
elif ">" in line:
|
|
359
|
+
pkg_name, version = line.split(">", 1)
|
|
360
|
+
elif "<" in line:
|
|
361
|
+
pkg_name, version = line.split("<", 1)
|
|
362
|
+
else:
|
|
363
|
+
pkg_name, version = line, "unknown"
|
|
364
|
+
|
|
365
|
+
pkg_name = pkg_name.strip()
|
|
366
|
+
version = version.strip()
|
|
367
|
+
|
|
368
|
+
findings.append(
|
|
369
|
+
self._mkf(
|
|
370
|
+
severity=Severity.INFO,
|
|
371
|
+
file=rel_path,
|
|
372
|
+
line_start=i,
|
|
373
|
+
title=f"Dependency: {pkg_name}@{version}",
|
|
374
|
+
description="Python dependency",
|
|
375
|
+
evidence=line,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Check for vulnerabilities via OSV API
|
|
380
|
+
vulns = self._check_pypi_vulnerabilities(pkg_name, version)
|
|
381
|
+
for vuln in vulns:
|
|
382
|
+
severity = self._map_osv_severity(
|
|
383
|
+
vuln.get("severity", [{}])[0].get("score", "UNKNOWN")
|
|
384
|
+
)
|
|
385
|
+
findings.append(
|
|
386
|
+
self._mkf(
|
|
387
|
+
severity=severity,
|
|
388
|
+
file=rel_path,
|
|
389
|
+
line_start=i,
|
|
390
|
+
title=f"VULNERABILITY: {pkg_name}@{version}",
|
|
391
|
+
description=vuln.get("summary", "Unknown vulnerability"),
|
|
392
|
+
evidence=json.dumps(vuln),
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return findings
|
|
397
|
+
|
|
398
|
+
def _scan_pipfile(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
399
|
+
"""Scan Pipfile for dependencies."""
|
|
400
|
+
findings = []
|
|
401
|
+
|
|
402
|
+
# Pipfiles are TOML format, but Python < 3.11 doesn't have built-in TOML support
|
|
403
|
+
# So we'll use regex for basic parsing
|
|
404
|
+
lines = content.splitlines()
|
|
405
|
+
current_section = ""
|
|
406
|
+
|
|
407
|
+
for i, line in enumerate(lines, 1):
|
|
408
|
+
line = line.strip()
|
|
409
|
+
|
|
410
|
+
# Identify section
|
|
411
|
+
if line.startswith("["):
|
|
412
|
+
current_section = line.strip("[]").strip()
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
# Look for package declarations in the right sections
|
|
416
|
+
if current_section in ["packages", "dev-packages"]:
|
|
417
|
+
pkg_match = re.match(r"^(\w[-\w]*)\s*=\s*", line)
|
|
418
|
+
if pkg_match:
|
|
419
|
+
pkg_name = pkg_match.group(1)
|
|
420
|
+
# Extract version from the rest of the line
|
|
421
|
+
version_part = line[len(pkg_name) :].strip("= ").strip('"')
|
|
422
|
+
findings.append(
|
|
423
|
+
self._mkf(
|
|
424
|
+
severity=Severity.INFO,
|
|
425
|
+
file=rel_path,
|
|
426
|
+
line_start=i,
|
|
427
|
+
title=f"Dependency: {pkg_name}@{version_part}",
|
|
428
|
+
description="Python dependency from Pipfile",
|
|
429
|
+
evidence=line,
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return findings
|
|
434
|
+
|
|
435
|
+
def _scan_poetry_lock(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
436
|
+
"""Scan poetry.lock for dependencies."""
|
|
437
|
+
findings = []
|
|
438
|
+
|
|
439
|
+
# Poetry lock files are TOML format
|
|
440
|
+
lines = content.splitlines()
|
|
441
|
+
current_package = {}
|
|
442
|
+
|
|
443
|
+
for i, line in enumerate(lines, 1):
|
|
444
|
+
line = line.strip()
|
|
445
|
+
|
|
446
|
+
# Look for package entries
|
|
447
|
+
if line.startswith("[[package]]"):
|
|
448
|
+
# Process previous package if exists
|
|
449
|
+
if current_package:
|
|
450
|
+
pkg_name = current_package.get("name", "unknown")
|
|
451
|
+
version = current_package.get("version", "unknown")
|
|
452
|
+
findings.append(
|
|
453
|
+
self._mkf(
|
|
454
|
+
severity=Severity.INFO,
|
|
455
|
+
file=rel_path,
|
|
456
|
+
line_start=i,
|
|
457
|
+
title=f"Dependency: {pkg_name}@{version}",
|
|
458
|
+
description="Python dependency from Poetry lock",
|
|
459
|
+
evidence=f"{pkg_name}=={version}",
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
current_package = {}
|
|
464
|
+
elif "=" in line and current_package is not None:
|
|
465
|
+
# Parse key=value pairs
|
|
466
|
+
key, value = line.split("=", 1)
|
|
467
|
+
key = key.strip().strip('"')
|
|
468
|
+
value = value.strip().strip('"')
|
|
469
|
+
current_package[key] = value
|
|
470
|
+
|
|
471
|
+
# Process the last package
|
|
472
|
+
if current_package:
|
|
473
|
+
pkg_name = current_package.get("name", "unknown")
|
|
474
|
+
version = current_package.get("version", "unknown")
|
|
475
|
+
findings.append(
|
|
476
|
+
self._mkf(
|
|
477
|
+
severity=Severity.INFO,
|
|
478
|
+
file=rel_path,
|
|
479
|
+
line_start=len(lines),
|
|
480
|
+
title=f"Dependency: {pkg_name}@{version}",
|
|
481
|
+
description="Python dependency from Poetry lock",
|
|
482
|
+
evidence=f"{pkg_name}=={version}",
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return findings
|
|
487
|
+
|
|
488
|
+
def _scan_cargo_file(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
489
|
+
"""Scan Cargo.toml or Cargo.lock for dependencies."""
|
|
490
|
+
findings = []
|
|
491
|
+
|
|
492
|
+
lines = content.splitlines()
|
|
493
|
+
in_deps_section = False
|
|
494
|
+
|
|
495
|
+
for i, line in enumerate(lines, 1):
|
|
496
|
+
line = line.strip()
|
|
497
|
+
|
|
498
|
+
# Identify dependencies section
|
|
499
|
+
if line.startswith("[dependencies]") or line.startswith("[dev-dependencies]"):
|
|
500
|
+
in_deps_section = True
|
|
501
|
+
continue
|
|
502
|
+
elif line.startswith("[") and in_deps_section:
|
|
503
|
+
in_deps_section = False
|
|
504
|
+
|
|
505
|
+
# Look for package declarations in dependencies section
|
|
506
|
+
if in_deps_section and "=" in line:
|
|
507
|
+
pkg_match = re.match(r"^(\w[\w\-_]*)\s*=", line)
|
|
508
|
+
if pkg_match:
|
|
509
|
+
pkg_name = pkg_match.group(1)
|
|
510
|
+
# Extract version from the rest of the line
|
|
511
|
+
version_part = line[len(pkg_name) :].strip("= ").strip('"')
|
|
512
|
+
findings.append(
|
|
513
|
+
self._mkf(
|
|
514
|
+
severity=Severity.INFO,
|
|
515
|
+
file=rel_path,
|
|
516
|
+
line_start=i,
|
|
517
|
+
title=f"Dependency: {pkg_name}@{version_part}",
|
|
518
|
+
description="Rust dependency",
|
|
519
|
+
evidence=line,
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
return findings
|
|
524
|
+
|
|
525
|
+
def _scan_go_mod(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
526
|
+
"""Scan go.mod for dependencies."""
|
|
527
|
+
findings = []
|
|
528
|
+
|
|
529
|
+
lines = content.splitlines()
|
|
530
|
+
|
|
531
|
+
for i, line in enumerate(lines, 1):
|
|
532
|
+
line = line.strip()
|
|
533
|
+
|
|
534
|
+
# Look for require statements
|
|
535
|
+
if line.startswith("require "):
|
|
536
|
+
# Extract package and version
|
|
537
|
+
req_parts = line[8:].strip().split() # Remove "require " prefix
|
|
538
|
+
if len(req_parts) >= 2:
|
|
539
|
+
pkg_name = req_parts[0]
|
|
540
|
+
version = req_parts[1]
|
|
541
|
+
findings.append(
|
|
542
|
+
self._mkf(
|
|
543
|
+
severity=Severity.INFO,
|
|
544
|
+
file=rel_path,
|
|
545
|
+
line_start=i,
|
|
546
|
+
title=f"Dependency: {pkg_name}@{version}",
|
|
547
|
+
description="Go dependency",
|
|
548
|
+
evidence=line,
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return findings
|
|
553
|
+
|
|
554
|
+
def _scan_composer_json(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
555
|
+
"""Scan composer.json for PHP dependencies."""
|
|
556
|
+
findings = []
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
data = json.loads(content)
|
|
560
|
+
deps = data.get("require", {})
|
|
561
|
+
dev_deps = data.get("require-dev", {})
|
|
562
|
+
|
|
563
|
+
# Combine all dependencies
|
|
564
|
+
all_deps = {}
|
|
565
|
+
all_deps.update(dict(deps.items()))
|
|
566
|
+
all_deps.update(dict(dev_deps.items()))
|
|
567
|
+
|
|
568
|
+
for name, version in all_deps.items():
|
|
569
|
+
findings.append(
|
|
570
|
+
self._mkf(
|
|
571
|
+
severity=Severity.INFO,
|
|
572
|
+
file=rel_path,
|
|
573
|
+
line_start=0,
|
|
574
|
+
title=f"Dependency: {name}@{version}",
|
|
575
|
+
description="PHP dependency",
|
|
576
|
+
evidence=f"Version: {version}",
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
except json.JSONDecodeError as e:
|
|
581
|
+
findings.append(
|
|
582
|
+
self._mkf(
|
|
583
|
+
severity=Severity.HIGH,
|
|
584
|
+
file=rel_path,
|
|
585
|
+
line_start=e.lineno,
|
|
586
|
+
title="Invalid composer.json",
|
|
587
|
+
description=f"JSON syntax error: {str(e)}",
|
|
588
|
+
evidence=e.msg,
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
return findings
|
|
593
|
+
|
|
594
|
+
def _scan_gemfile(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
595
|
+
"""Scan Gemfile or Gemfile.lock for Ruby dependencies."""
|
|
596
|
+
findings = []
|
|
597
|
+
|
|
598
|
+
lines = content.splitlines()
|
|
599
|
+
|
|
600
|
+
for i, line in enumerate(lines, 1):
|
|
601
|
+
line = line.strip()
|
|
602
|
+
|
|
603
|
+
# Look for gem statements
|
|
604
|
+
if "gem " in line and not line.startswith("#"):
|
|
605
|
+
# Extract gem name
|
|
606
|
+
gem_match = re.search(r'gem\s+["\']([^"\']+)["\']', line)
|
|
607
|
+
if gem_match:
|
|
608
|
+
gem_name = gem_match.group(1)
|
|
609
|
+
findings.append(
|
|
610
|
+
self._mkf(
|
|
611
|
+
severity=Severity.INFO,
|
|
612
|
+
file=rel_path,
|
|
613
|
+
line_start=i,
|
|
614
|
+
title=f"Dependency: {gem_name}",
|
|
615
|
+
description="Ruby dependency",
|
|
616
|
+
evidence=line,
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
return findings
|
|
621
|
+
|
|
622
|
+
def _scan_lock_file(self, file_path: Path, rel_path: str, content: str) -> list[Finding]:
|
|
623
|
+
"""Scan lock files (yarn.lock, pnpm-lock.yaml) for dependencies."""
|
|
624
|
+
findings = []
|
|
625
|
+
|
|
626
|
+
lines = content.splitlines()
|
|
627
|
+
|
|
628
|
+
# Different parsing for different lock file types
|
|
629
|
+
if file_path.name == "yarn.lock":
|
|
630
|
+
# Yarn lock files have "package@version" entries
|
|
631
|
+
for i, line in enumerate(lines, 1):
|
|
632
|
+
if line.strip().endswith(":") and "@" in line:
|
|
633
|
+
# Extract package and version
|
|
634
|
+
pkg_part = line.strip().rstrip(":")
|
|
635
|
+
if "@" in pkg_part:
|
|
636
|
+
pkg_name = pkg_part.split("@")[0]
|
|
637
|
+
findings.append(
|
|
638
|
+
self._mkf(
|
|
639
|
+
severity=Severity.INFO,
|
|
640
|
+
file=rel_path,
|
|
641
|
+
line_start=i,
|
|
642
|
+
title=f"Dependency: {pkg_name}",
|
|
643
|
+
description="JavaScript dependency from lock file",
|
|
644
|
+
evidence=pkg_part,
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
elif file_path.name == "pnpm-lock.yaml":
|
|
648
|
+
# PNPM lock files have similar format to yarn
|
|
649
|
+
for i, line in enumerate(lines, 1):
|
|
650
|
+
if line.strip().endswith(":") and "/" in line:
|
|
651
|
+
# Extract package name
|
|
652
|
+
pkg_part = line.strip().rstrip(":")
|
|
653
|
+
if "/" in pkg_part and "@" in pkg_part:
|
|
654
|
+
pkg_name = pkg_part.split("/")[-1].split("@")[0]
|
|
655
|
+
findings.append(
|
|
656
|
+
self._mkf(
|
|
657
|
+
severity=Severity.INFO,
|
|
658
|
+
file=rel_path,
|
|
659
|
+
line_start=i,
|
|
660
|
+
title=f"Dependency: {pkg_name}",
|
|
661
|
+
description="JavaScript dependency from pnpm lock",
|
|
662
|
+
evidence=pkg_part,
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
return findings
|
|
667
|
+
|
|
668
|
+
def _check_npm_vulnerabilities(self, package_name: str, version: str) -> list[dict]:
|
|
669
|
+
"""Check npm package for vulnerabilities via OSV API."""
|
|
670
|
+
if is_offline():
|
|
671
|
+
return []
|
|
672
|
+
try:
|
|
673
|
+
url = "https://api.osv.dev/v1/query"
|
|
674
|
+
query = {"package": {"name": package_name, "ecosystem": "npm"}, "version": version}
|
|
675
|
+
|
|
676
|
+
req = urllib.request.Request(
|
|
677
|
+
url,
|
|
678
|
+
data=json.dumps(query).encode("utf-8"),
|
|
679
|
+
headers={"Content-Type": "application/json"},
|
|
680
|
+
)
|
|
681
|
+
response = urllib.request.urlopen(req, timeout=15)
|
|
682
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
683
|
+
|
|
684
|
+
return data.get("vulns", [])
|
|
685
|
+
except Exception as e:
|
|
686
|
+
# Return empty list if API call fails
|
|
687
|
+
_log.warning("DependencyScanner._check_npm_vulnerabilities failed: %s", e)
|
|
688
|
+
return []
|
|
689
|
+
|
|
690
|
+
def _check_pypi_vulnerabilities(self, package_name: str, version: str) -> list[dict]:
|
|
691
|
+
"""Check PyPI package for vulnerabilities via OSV API."""
|
|
692
|
+
if is_offline():
|
|
693
|
+
return []
|
|
694
|
+
try:
|
|
695
|
+
url = "https://api.osv.dev/v1/query"
|
|
696
|
+
query = {"package": {"name": package_name, "ecosystem": "PyPI"}, "version": version}
|
|
697
|
+
|
|
698
|
+
req = urllib.request.Request(
|
|
699
|
+
url,
|
|
700
|
+
data=json.dumps(query).encode("utf-8"),
|
|
701
|
+
headers={"Content-Type": "application/json"},
|
|
702
|
+
)
|
|
703
|
+
response = urllib.request.urlopen(req, timeout=15)
|
|
704
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
705
|
+
|
|
706
|
+
return data.get("vulns", [])
|
|
707
|
+
except Exception as e:
|
|
708
|
+
# Return empty list if API call fails
|
|
709
|
+
_log.warning("DependencyScanner._check_pypi_vulnerabilities failed: %s", e)
|
|
710
|
+
return []
|
|
711
|
+
|
|
712
|
+
def _map_osv_severity(self, osv_severity: str) -> Severity:
|
|
713
|
+
"""Map OSV severity to our severity levels."""
|
|
714
|
+
if not osv_severity or osv_severity == "UNKNOWN":
|
|
715
|
+
return Severity.LOW
|
|
716
|
+
|
|
717
|
+
# Convert to uppercase for comparison
|
|
718
|
+
sev = osv_severity.upper()
|
|
719
|
+
|
|
720
|
+
if sev in ["CRITICAL", "HIGH"]:
|
|
721
|
+
return Severity.CRITICAL
|
|
722
|
+
elif sev == "MEDIUM":
|
|
723
|
+
return Severity.MEDIUM
|
|
724
|
+
else:
|
|
725
|
+
return Severity.LOW
|