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,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Container Scanner — scans container images and Dockerfiles for vulnerabilities.
|
|
3
|
+
|
|
4
|
+
Wraps Trivy (optional subprocess) for container image CVE scanning.
|
|
5
|
+
Falls back to static Dockerfile analysis when Trivy is not available.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
from patchi.core.agents.base import (
|
|
16
|
+
AgentGroup,
|
|
17
|
+
AgentInput,
|
|
18
|
+
AgentResult,
|
|
19
|
+
BaseAgent,
|
|
20
|
+
Finding,
|
|
21
|
+
Severity,
|
|
22
|
+
register,
|
|
23
|
+
safe_rglob,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@register
|
|
28
|
+
class ContainerScannerAgent(BaseAgent):
|
|
29
|
+
"""Scans container images and Dockerfiles for CVEs and misconfigurations."""
|
|
30
|
+
|
|
31
|
+
name = "ContainerScannerAgent"
|
|
32
|
+
group = AgentGroup.SECURITY
|
|
33
|
+
timeout = 120
|
|
34
|
+
|
|
35
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
36
|
+
scanned = 0
|
|
37
|
+
|
|
38
|
+
# Try Trivy image scan if available
|
|
39
|
+
trivy_available = self._check_trivy()
|
|
40
|
+
if trivy_available:
|
|
41
|
+
scanned += self._scan_with_trivy(inp, result)
|
|
42
|
+
|
|
43
|
+
# Always do static Dockerfile analysis
|
|
44
|
+
scanned += self._scan_dockerfiles(inp, result)
|
|
45
|
+
|
|
46
|
+
# Scan docker-compose for image references
|
|
47
|
+
scanned += self._scan_compose_images(inp, result)
|
|
48
|
+
|
|
49
|
+
result.files_scanned = scanned
|
|
50
|
+
|
|
51
|
+
def _check_trivy(self) -> bool:
|
|
52
|
+
try:
|
|
53
|
+
proc = subprocess.run(
|
|
54
|
+
["trivy", "--version"],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=5,
|
|
58
|
+
)
|
|
59
|
+
return proc.returncode == 0
|
|
60
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def _scan_with_trivy(self, inp: AgentInput, result: AgentResult) -> int:
|
|
64
|
+
"""Scan Dockerfiles with Trivy config scanner."""
|
|
65
|
+
count = 0
|
|
66
|
+
for fpath in safe_rglob(inp.root, "Dockerfile*"):
|
|
67
|
+
if not fpath.is_file():
|
|
68
|
+
continue
|
|
69
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
70
|
+
try:
|
|
71
|
+
proc = subprocess.run(
|
|
72
|
+
["trivy", "config", "--format=json", str(fpath)],
|
|
73
|
+
capture_output=True,
|
|
74
|
+
text=True,
|
|
75
|
+
timeout=60,
|
|
76
|
+
)
|
|
77
|
+
if proc.stdout.strip():
|
|
78
|
+
data = json.loads(proc.stdout)
|
|
79
|
+
for target in data.get("Results", []):
|
|
80
|
+
for vuln in target.get("Vulnerabilities", []):
|
|
81
|
+
sev = vuln.get("Severity", "MEDIUM").upper()
|
|
82
|
+
result.add_finding(
|
|
83
|
+
Finding(
|
|
84
|
+
agent=self.name,
|
|
85
|
+
type="container_cve",
|
|
86
|
+
severity=Severity(sev.lower())
|
|
87
|
+
if sev.lower() in ("critical", "high", "medium", "low")
|
|
88
|
+
else Severity.MEDIUM,
|
|
89
|
+
file=rel,
|
|
90
|
+
message=f"{vuln.get('VulnerabilityID', '')}: {vuln.get('Title', 'Container vulnerability')}",
|
|
91
|
+
cwe=vuln.get("CweIDs", [""])[0] if vuln.get("CweIDs") else "",
|
|
92
|
+
extra={
|
|
93
|
+
"vuln_id": vuln.get("VulnerabilityID", ""),
|
|
94
|
+
"pkg": vuln.get("PkgName", ""),
|
|
95
|
+
"installed": vuln.get("InstalledVersion", ""),
|
|
96
|
+
"fixed": vuln.get("FixedVersion", ""),
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception):
|
|
101
|
+
pass
|
|
102
|
+
count += 1
|
|
103
|
+
return count
|
|
104
|
+
|
|
105
|
+
def _check_layer_bloat(self, content: str, rel: str, result: AgentResult) -> None:
|
|
106
|
+
"""§8.3.3 dive / scout heuristic: layer bloat detection §8.3.3."""
|
|
107
|
+
import shutil as _sh
|
|
108
|
+
import subprocess as _sp
|
|
109
|
+
|
|
110
|
+
# Try dive CI if installed
|
|
111
|
+
if _sh.which("dive"):
|
|
112
|
+
try:
|
|
113
|
+
proc = _sp.run(
|
|
114
|
+
["dive", "--ci", "--json", rel],
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
timeout=30,
|
|
118
|
+
)
|
|
119
|
+
if proc.stdout:
|
|
120
|
+
import json as _js
|
|
121
|
+
|
|
122
|
+
data = _js.loads(proc.stdout)
|
|
123
|
+
eff = data.get("efficiencyScore", 1.0)
|
|
124
|
+
if eff < 0.9:
|
|
125
|
+
result.add_finding(
|
|
126
|
+
Finding(
|
|
127
|
+
agent=self.name,
|
|
128
|
+
type="layer_bloat",
|
|
129
|
+
severity=Severity.MEDIUM,
|
|
130
|
+
file=rel,
|
|
131
|
+
message=f"Dive efficiency {eff:.0%} — image has wasted space / duplicate layers",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
return # dive succeeded, skip heuristic
|
|
135
|
+
except Exception as _exc:
|
|
136
|
+
logging.getLogger("patchi").debug('suppressed: %s', _exc)
|
|
137
|
+
# Heuristic fallback: count RUN, check multi-stage, large base
|
|
138
|
+
runs = len(re.findall(r"^\s*RUN\s+", content, re.MULTILINE | re.I))
|
|
139
|
+
froms = re.findall(r"^\s*FROM\s+(\S+)", content, re.MULTILINE | re.I)
|
|
140
|
+
if runs > 7:
|
|
141
|
+
result.add_finding(
|
|
142
|
+
Finding(
|
|
143
|
+
agent=self.name,
|
|
144
|
+
type="layer_bloat",
|
|
145
|
+
severity=Severity.LOW,
|
|
146
|
+
file=rel,
|
|
147
|
+
message=f"{runs} RUN layers — consolidate with && \\ to reduce layers (dive: layer bloat)",
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
if len(froms) == 1 and runs > 5:
|
|
151
|
+
result.add_finding(
|
|
152
|
+
Finding(
|
|
153
|
+
agent=self.name,
|
|
154
|
+
type="no_multistage",
|
|
155
|
+
severity=Severity.LOW,
|
|
156
|
+
file=rel,
|
|
157
|
+
message="Single-stage build with many RUNs — consider multi-stage to slim final image",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
for base in froms:
|
|
161
|
+
if any(b in base.lower() for b in (":latest", "ubuntu:latest", "alpine:latest")) or ":" not in base:
|
|
162
|
+
result.add_finding(
|
|
163
|
+
Finding(
|
|
164
|
+
agent=self.name,
|
|
165
|
+
type="unpinned_base",
|
|
166
|
+
severity=Severity.MEDIUM,
|
|
167
|
+
file=rel,
|
|
168
|
+
message=f"Unpinned base image: {base} — pin to digest or specific tag",
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _scan_dockerfiles(self, inp: AgentInput, result: AgentResult) -> int:
|
|
173
|
+
"""Static Dockerfile analysis when Trivy is not available."""
|
|
174
|
+
count = 0
|
|
175
|
+
for fpath in safe_rglob(inp.root, "Dockerfile*"):
|
|
176
|
+
if not fpath.is_file():
|
|
177
|
+
continue
|
|
178
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
179
|
+
try:
|
|
180
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
181
|
+
except OSError:
|
|
182
|
+
continue
|
|
183
|
+
count += 1
|
|
184
|
+
|
|
185
|
+
lines = content.splitlines()
|
|
186
|
+
has_healthcheck = False
|
|
187
|
+
has_copy_add = False
|
|
188
|
+
|
|
189
|
+
for i, line in enumerate(lines, 1):
|
|
190
|
+
stripped = line.strip()
|
|
191
|
+
if stripped.startswith("#"):
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
upper = stripped.upper()
|
|
195
|
+
|
|
196
|
+
# HEALTHCHECK
|
|
197
|
+
if upper.startswith("HEALTHCHECK"):
|
|
198
|
+
has_healthcheck = True
|
|
199
|
+
|
|
200
|
+
# COPY vs ADD
|
|
201
|
+
if upper.startswith("ADD ") and not has_copy_add:
|
|
202
|
+
has_copy_add = True
|
|
203
|
+
result.add_finding(
|
|
204
|
+
Finding(
|
|
205
|
+
agent=self.name,
|
|
206
|
+
type="use_copy_over_add",
|
|
207
|
+
severity=Severity.LOW,
|
|
208
|
+
file=rel,
|
|
209
|
+
line=i,
|
|
210
|
+
message="Consider using COPY instead of ADD (ADD has implicit tar extraction)",
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Secrets in ENV
|
|
215
|
+
if upper.startswith("ENV "):
|
|
216
|
+
secretish = re.search(
|
|
217
|
+
r"(?:password|secret|key|token|api_key)\s*=\s*\S+",
|
|
218
|
+
stripped,
|
|
219
|
+
re.I,
|
|
220
|
+
)
|
|
221
|
+
if secretish:
|
|
222
|
+
result.add_finding(
|
|
223
|
+
Finding(
|
|
224
|
+
agent=self.name,
|
|
225
|
+
type="env_secret",
|
|
226
|
+
severity=Severity.CRITICAL,
|
|
227
|
+
file=rel,
|
|
228
|
+
line=i,
|
|
229
|
+
message="Secret in ENV — use build-time secrets or runtime injection",
|
|
230
|
+
cwe="CWE-798",
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if not has_healthcheck and lines:
|
|
235
|
+
result.add_finding(
|
|
236
|
+
Finding(
|
|
237
|
+
agent=self.name,
|
|
238
|
+
type="no_healthcheck",
|
|
239
|
+
severity=Severity.LOW,
|
|
240
|
+
file=rel,
|
|
241
|
+
message="No HEALTHCHECK instruction — container health unknown to orchestrator",
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Layer bloat check §8.3.3
|
|
246
|
+
self._check_layer_bloat(content, rel, result)
|
|
247
|
+
|
|
248
|
+
return count
|
|
249
|
+
|
|
250
|
+
def _scan_compose_images(self, inp: AgentInput, result: AgentResult) -> int:
|
|
251
|
+
"""Check docker-compose for :latest tags and unpinned images."""
|
|
252
|
+
count = 0
|
|
253
|
+
for pattern in [
|
|
254
|
+
"docker-compose*.yml",
|
|
255
|
+
"docker-compose*.yaml",
|
|
256
|
+
"compose.yml",
|
|
257
|
+
"compose.yaml",
|
|
258
|
+
]:
|
|
259
|
+
for fpath in safe_rglob(inp.root, pattern):
|
|
260
|
+
if not fpath.is_file():
|
|
261
|
+
continue
|
|
262
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
263
|
+
try:
|
|
264
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
265
|
+
except OSError:
|
|
266
|
+
continue
|
|
267
|
+
count += 1
|
|
268
|
+
|
|
269
|
+
for m in re.finditer(r'image:\s*["\']?([^"\'\s]+)["\']?', content):
|
|
270
|
+
img = m.group(1)
|
|
271
|
+
if ":latest" in img or ":" not in img:
|
|
272
|
+
result.add_finding(
|
|
273
|
+
Finding(
|
|
274
|
+
agent=self.name,
|
|
275
|
+
type="unpinned_image",
|
|
276
|
+
severity=Severity.MEDIUM,
|
|
277
|
+
file=rel,
|
|
278
|
+
message=f"Unpinned or :latest image: {img}",
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return count
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CryptoAgent — cryptographic implementation issues.
|
|
3
|
+
|
|
4
|
+
Detects crypto-related security issues:
|
|
5
|
+
- Weak hashing algorithms (MD5, SHA1)
|
|
6
|
+
- Weak encryption algorithms (DES, RC4)
|
|
7
|
+
- Hardcoded cryptographic keys
|
|
8
|
+
- Improper salt usage
|
|
9
|
+
- Predictable random number generation
|
|
10
|
+
- Padding oracle vulnerabilities
|
|
11
|
+
- Weak key sizes
|
|
12
|
+
- Insecure PRNG usage
|
|
13
|
+
|
|
14
|
+
Uses pattern matching and code analysis.
|
|
15
|
+
Does NOT call AI.
|
|
16
|
+
Does NOT write to disk.
|
|
17
|
+
Does NOT touch the queue.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from ..agents.base import (
|
|
26
|
+
AgentDomain,
|
|
27
|
+
AgentGroup,
|
|
28
|
+
AgentInput,
|
|
29
|
+
AgentResult,
|
|
30
|
+
AgentStatus,
|
|
31
|
+
BaseAgent,
|
|
32
|
+
Finding,
|
|
33
|
+
Severity,
|
|
34
|
+
make_finding,
|
|
35
|
+
register,
|
|
36
|
+
safe_rglob,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@register
|
|
41
|
+
class CryptoAgent(BaseAgent):
|
|
42
|
+
"""Agent for detecting cryptographic implementation issues."""
|
|
43
|
+
|
|
44
|
+
group = AgentGroup.SECURITY
|
|
45
|
+
domain = AgentDomain.SECURITY
|
|
46
|
+
name = "CryptoAgent"
|
|
47
|
+
description = "Cryptographic issues: weak algos, hardcoded keys, improper salt, PRNG"
|
|
48
|
+
|
|
49
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
50
|
+
"""Run cryptographic security detection."""
|
|
51
|
+
findings = []
|
|
52
|
+
|
|
53
|
+
# Define source file patterns to scan
|
|
54
|
+
source_patterns = [
|
|
55
|
+
"*.py",
|
|
56
|
+
"*.js",
|
|
57
|
+
"*.jsx",
|
|
58
|
+
"*.ts",
|
|
59
|
+
"*.tsx",
|
|
60
|
+
"*.java",
|
|
61
|
+
"*.php",
|
|
62
|
+
"*.rb",
|
|
63
|
+
"*.go",
|
|
64
|
+
"*.rs",
|
|
65
|
+
"*.cpp",
|
|
66
|
+
"*.cxx",
|
|
67
|
+
"*.cc",
|
|
68
|
+
"*.c",
|
|
69
|
+
"*.h",
|
|
70
|
+
"*.hpp",
|
|
71
|
+
"*.cs",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Search for source files
|
|
75
|
+
for pattern in source_patterns:
|
|
76
|
+
for file_path in safe_rglob(inp.root, pattern):
|
|
77
|
+
if file_path.is_file():
|
|
78
|
+
rel_path = file_path.relative_to(inp.root).as_posix()
|
|
79
|
+
if not self._should_skip_file(rel_path, inp):
|
|
80
|
+
findings.extend(self._scan_file_crypto_security(file_path, rel_path))
|
|
81
|
+
|
|
82
|
+
result.status = AgentStatus.SUCCEEDED
|
|
83
|
+
result.findings = findings
|
|
84
|
+
result.data.update(
|
|
85
|
+
{
|
|
86
|
+
"crypto_findings": len(
|
|
87
|
+
[
|
|
88
|
+
f
|
|
89
|
+
for f in findings
|
|
90
|
+
if any(
|
|
91
|
+
word in f.title.lower()
|
|
92
|
+
for word in [
|
|
93
|
+
"crypto",
|
|
94
|
+
"hash",
|
|
95
|
+
"encrypt",
|
|
96
|
+
"random",
|
|
97
|
+
"salt",
|
|
98
|
+
"key",
|
|
99
|
+
"prng",
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
]
|
|
103
|
+
),
|
|
104
|
+
"needs_ai": False,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
|
|
110
|
+
"""Check if file should be skipped based on restrictions."""
|
|
111
|
+
from pathlib import PurePosixPath
|
|
112
|
+
|
|
113
|
+
# Check restrictions
|
|
114
|
+
restrictions = inp.config.get("restrictions", [])
|
|
115
|
+
for r in restrictions:
|
|
116
|
+
if r.get("enabled", True):
|
|
117
|
+
path = r["path"]
|
|
118
|
+
if file_path.startswith(path) or PurePosixPath(file_path).match(path):
|
|
119
|
+
if r["type"] == "NO_TOUCH":
|
|
120
|
+
return True
|
|
121
|
+
elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def _scan_file_crypto_security(self, file_path: Path, rel_path: str) -> list[Finding]:
|
|
126
|
+
"""Scan a file for cryptographic security issues."""
|
|
127
|
+
findings = []
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
content = file_path.read_text(encoding="utf-8")
|
|
131
|
+
|
|
132
|
+
# Check for weak hashing algorithms
|
|
133
|
+
findings.extend(self._scan_weak_hashing(content, rel_path))
|
|
134
|
+
|
|
135
|
+
# Check for weak encryption algorithms
|
|
136
|
+
findings.extend(self._scan_weak_encryption(content, rel_path))
|
|
137
|
+
|
|
138
|
+
# Check for hardcoded cryptographic keys
|
|
139
|
+
findings.extend(self._scan_hardcoded_keys(content, rel_path))
|
|
140
|
+
|
|
141
|
+
# Check for improper salt usage
|
|
142
|
+
findings.extend(self._scan_salt_issues(content, rel_path))
|
|
143
|
+
|
|
144
|
+
# Check for predictable random generation
|
|
145
|
+
findings.extend(self._scan_random_generation(content, rel_path))
|
|
146
|
+
|
|
147
|
+
# Check for other crypto issues
|
|
148
|
+
findings.extend(self._scan_other_crypto_issues(content, rel_path))
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
findings.append(
|
|
152
|
+
make_finding(
|
|
153
|
+
severity=Severity.LOW,
|
|
154
|
+
file=rel_path,
|
|
155
|
+
line_start=0,
|
|
156
|
+
title="Crypto security scanner file read error",
|
|
157
|
+
description=f"Could not analyze {file_path.name} for cryptographic issues: {str(e)}",
|
|
158
|
+
evidence=str(e),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return findings
|
|
163
|
+
|
|
164
|
+
def _scan_weak_hashing(self, content: str, rel_path: str) -> list[Finding]:
|
|
165
|
+
"""Scan for weak hashing algorithm usage."""
|
|
166
|
+
findings = []
|
|
167
|
+
|
|
168
|
+
# Look for weak hashing algorithms
|
|
169
|
+
weak_hash_patterns = [
|
|
170
|
+
(r"MD5|md5", "Weak Hash Algorithm (MD5)", Severity.HIGH),
|
|
171
|
+
(r"SHA1|sha1", "Weak Hash Algorithm (SHA1)", Severity.HIGH),
|
|
172
|
+
(r"hash.*md5|digest.*md5", "MD5 Hash Usage", Severity.HIGH),
|
|
173
|
+
(r"hash.*sha1|digest.*sha1", "SHA1 Hash Usage", Severity.HIGH),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
lines = content.splitlines()
|
|
177
|
+
for i, line in enumerate(lines, 1):
|
|
178
|
+
for pattern, description, severity in weak_hash_patterns:
|
|
179
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
180
|
+
for match in matches:
|
|
181
|
+
findings.append(
|
|
182
|
+
make_finding(
|
|
183
|
+
severity=severity,
|
|
184
|
+
file=rel_path,
|
|
185
|
+
line_start=i,
|
|
186
|
+
title=description,
|
|
187
|
+
description=f"Weak hashing algorithm found: {match.group(0)}",
|
|
188
|
+
evidence=line.strip(),
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return findings
|
|
193
|
+
|
|
194
|
+
def _scan_weak_encryption(self, content: str, rel_path: str) -> list[Finding]:
|
|
195
|
+
"""Scan for weak encryption algorithm usage."""
|
|
196
|
+
findings = []
|
|
197
|
+
|
|
198
|
+
# Look for weak encryption algorithms
|
|
199
|
+
weak_crypto_patterns = [
|
|
200
|
+
(r"\bDES\b", "Weak Encryption Algorithm (DES)", Severity.HIGH),
|
|
201
|
+
(r"\bRC4\b", "Weak Stream Cipher (RC4)", Severity.HIGH),
|
|
202
|
+
(r"\bblowfish\b", "Weak Block Cipher (Blowfish)", Severity.MEDIUM),
|
|
203
|
+
(r"encrypt.*\bDES\b|cipher.*\bDES\b", "DES Encryption Usage", Severity.HIGH),
|
|
204
|
+
(r"encrypt.*\bRC4\b|cipher.*\bRC4\b", "RC4 Encryption Usage", Severity.HIGH),
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
lines = content.splitlines()
|
|
208
|
+
for i, line in enumerate(lines, 1):
|
|
209
|
+
for pattern, description, severity in weak_crypto_patterns:
|
|
210
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
211
|
+
for match in matches:
|
|
212
|
+
findings.append(
|
|
213
|
+
make_finding(
|
|
214
|
+
severity=severity,
|
|
215
|
+
file=rel_path,
|
|
216
|
+
line_start=i,
|
|
217
|
+
title=description,
|
|
218
|
+
description=f"Weak encryption algorithm found: {match.group(0)}",
|
|
219
|
+
evidence=line.strip(),
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return findings
|
|
224
|
+
|
|
225
|
+
def _scan_hardcoded_keys(self, content: str, rel_path: str) -> list[Finding]:
|
|
226
|
+
"""Scan for hardcoded cryptographic keys."""
|
|
227
|
+
findings = []
|
|
228
|
+
|
|
229
|
+
# Look for hardcoded keys (require key-like context to reduce false positives)
|
|
230
|
+
key_patterns = [
|
|
231
|
+
(
|
|
232
|
+
r'(?:\bsecret\b|\bkey\b|\btoken\b|\bpassword)\s*[:=]\s*["\'][A-Za-z0-9+/=]{20,}["\']',
|
|
233
|
+
"Hardcoded Cryptographic Key",
|
|
234
|
+
Severity.HIGH,
|
|
235
|
+
),
|
|
236
|
+
(
|
|
237
|
+
r'(?:SECRET_KEY|API_KEY|PRIVATE_KEY|ENCRYPTION_KEY)\s*[:=]\s*["\'][^"\']{8,}["\']',
|
|
238
|
+
"Hardcoded Key Constant",
|
|
239
|
+
Severity.HIGH,
|
|
240
|
+
),
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
lines = content.splitlines()
|
|
244
|
+
for i, line in enumerate(lines, 1):
|
|
245
|
+
for pattern, description, severity in key_patterns:
|
|
246
|
+
matches = re.finditer(pattern, line)
|
|
247
|
+
for match in matches:
|
|
248
|
+
# Additional validation to reduce false positives
|
|
249
|
+
matched_text = match.group(0)
|
|
250
|
+
# Check if it looks like a real key (not just a long hex string in other context)
|
|
251
|
+
if len(matched_text) > 20 or ("key" in line.lower() and len(matched_text) > 10):
|
|
252
|
+
findings.append(
|
|
253
|
+
make_finding(
|
|
254
|
+
severity=severity,
|
|
255
|
+
file=rel_path,
|
|
256
|
+
line_start=i,
|
|
257
|
+
title=description,
|
|
258
|
+
description=f"Potentially hardcoded cryptographic key found: {matched_text[:20]}...",
|
|
259
|
+
evidence=line.strip(),
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return findings
|
|
264
|
+
|
|
265
|
+
def _scan_salt_issues(self, content: str, rel_path: str) -> list[Finding]:
|
|
266
|
+
"""Scan for improper salt usage."""
|
|
267
|
+
findings = []
|
|
268
|
+
|
|
269
|
+
# Look for missing or weak salt usage
|
|
270
|
+
salt_patterns = [
|
|
271
|
+
(r"hash.*password(?![^=]*salt)", "Hashing Password Without Salt", Severity.HIGH),
|
|
272
|
+
(r"pbkdf2(?![^=]*salt)", "PBKDF2 Without Salt", Severity.HIGH),
|
|
273
|
+
(r"bcrypt(?![^=]*salt)", "bcrypt Without Salt", Severity.HIGH),
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
lines = content.splitlines()
|
|
277
|
+
for i, line in enumerate(lines, 1):
|
|
278
|
+
for pattern, description, severity in salt_patterns:
|
|
279
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
280
|
+
for _match in matches:
|
|
281
|
+
findings.append(
|
|
282
|
+
make_finding(
|
|
283
|
+
severity=severity,
|
|
284
|
+
file=rel_path,
|
|
285
|
+
line_start=i,
|
|
286
|
+
title=description,
|
|
287
|
+
description="Cryptographic function used without proper salt",
|
|
288
|
+
evidence=line.strip(),
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Look for hardcoded salts
|
|
293
|
+
hardcoded_salt_patterns = [
|
|
294
|
+
(r'\bsalt\b\s*[=:]\s*["\'][^"\']+["\']', "Hardcoded Salt", Severity.HIGH),
|
|
295
|
+
(
|
|
296
|
+
r"const.*\bsalt\b|var.*\bsalt\b|let.*\bsalt\b",
|
|
297
|
+
"Possible Hardcoded Salt Variable",
|
|
298
|
+
Severity.MEDIUM,
|
|
299
|
+
),
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
for i, line in enumerate(lines, 1):
|
|
303
|
+
for pattern, description, severity in hardcoded_salt_patterns:
|
|
304
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
305
|
+
for _match in matches:
|
|
306
|
+
findings.append(
|
|
307
|
+
make_finding(
|
|
308
|
+
severity=severity,
|
|
309
|
+
file=rel_path,
|
|
310
|
+
line_start=i,
|
|
311
|
+
title=description,
|
|
312
|
+
description="Possible hardcoded salt detected",
|
|
313
|
+
evidence=line.strip(),
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return findings
|
|
318
|
+
|
|
319
|
+
def _scan_random_generation(self, content: str, rel_path: str) -> list[Finding]:
|
|
320
|
+
"""Scan for predictable random number generation."""
|
|
321
|
+
findings = []
|
|
322
|
+
|
|
323
|
+
# Look for insecure random generation (only in crypto-relevant contexts)
|
|
324
|
+
random_patterns = [
|
|
325
|
+
(r"Math\.random\(\)", "Predictable Random Number (JavaScript)", Severity.HIGH),
|
|
326
|
+
(
|
|
327
|
+
r"(?:random\.random|random\.randint|random\.choice|random\.randrange)\b",
|
|
328
|
+
"Non-crypto PRNG (Python) — fine for games, risky for tokens/secrets",
|
|
329
|
+
Severity.MEDIUM,
|
|
330
|
+
),
|
|
331
|
+
(r"System\.Random|Random\.Next", "Potentially Weak Random (C#)", Severity.MEDIUM),
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
lines = content.splitlines()
|
|
335
|
+
for i, line in enumerate(lines, 1):
|
|
336
|
+
for pattern, description, severity in random_patterns:
|
|
337
|
+
matches = re.finditer(pattern, line)
|
|
338
|
+
for match in matches:
|
|
339
|
+
findings.append(
|
|
340
|
+
make_finding(
|
|
341
|
+
severity=severity,
|
|
342
|
+
file=rel_path,
|
|
343
|
+
line_start=i,
|
|
344
|
+
title=description,
|
|
345
|
+
description=f"Insecure random number generation: {match.group(0)}",
|
|
346
|
+
evidence=line.strip(),
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return findings
|
|
351
|
+
|
|
352
|
+
def _scan_other_crypto_issues(self, content: str, rel_path: str) -> list[Finding]:
|
|
353
|
+
"""Scan for other cryptographic issues."""
|
|
354
|
+
findings = []
|
|
355
|
+
|
|
356
|
+
# Look for weak key sizes
|
|
357
|
+
key_size_patterns = [
|
|
358
|
+
(r"key_size.*512|rsa.*512", "Weak RSA Key Size (512 bits)", Severity.HIGH),
|
|
359
|
+
(r"key_size.*1024|rsa.*1024", "Weak RSA Key Size (1024 bits)", Severity.MEDIUM),
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
lines = content.splitlines()
|
|
363
|
+
for i, line in enumerate(lines, 1):
|
|
364
|
+
for pattern, description, severity in key_size_patterns:
|
|
365
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
366
|
+
for match in matches:
|
|
367
|
+
findings.append(
|
|
368
|
+
make_finding(
|
|
369
|
+
severity=severity,
|
|
370
|
+
file=rel_path,
|
|
371
|
+
line_start=i,
|
|
372
|
+
title=description,
|
|
373
|
+
description=f"Weak cryptographic key size: {match.group(0)}",
|
|
374
|
+
evidence=line.strip(),
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return findings
|