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,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security History & Analytics (Layer 4) — scan history and findings lifecycle tracking.
|
|
3
|
+
|
|
4
|
+
SQLite-backed storage for:
|
|
5
|
+
- Scan history (every security scan recorded)
|
|
6
|
+
- Findings lifecycle (open → fixed → verified → false_positive)
|
|
7
|
+
- Trend analytics (findings over time, mean time-to-fix)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sqlite3
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from patchi.core.agents.base import (
|
|
19
|
+
AgentGroup,
|
|
20
|
+
AgentInput,
|
|
21
|
+
AgentResult,
|
|
22
|
+
BaseAgent,
|
|
23
|
+
Finding,
|
|
24
|
+
Severity,
|
|
25
|
+
register,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_DB_NAME = "patchi_history.db"
|
|
29
|
+
|
|
30
|
+
_CREATE_TABLES = """
|
|
31
|
+
CREATE TABLE IF NOT EXISTS scan_history (
|
|
32
|
+
scan_id TEXT PRIMARY KEY,
|
|
33
|
+
timestamp TEXT NOT NULL,
|
|
34
|
+
repo_path TEXT,
|
|
35
|
+
tool TEXT,
|
|
36
|
+
findings_count INTEGER DEFAULT 0,
|
|
37
|
+
severity_breakdown TEXT,
|
|
38
|
+
duration_ms INTEGER DEFAULT 0,
|
|
39
|
+
health_score INTEGER DEFAULT 0
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS findings (
|
|
43
|
+
finding_id TEXT PRIMARY KEY,
|
|
44
|
+
scan_id TEXT,
|
|
45
|
+
rule_id TEXT,
|
|
46
|
+
file TEXT,
|
|
47
|
+
line INTEGER DEFAULT 0,
|
|
48
|
+
severity TEXT,
|
|
49
|
+
status TEXT DEFAULT 'open',
|
|
50
|
+
first_seen TEXT,
|
|
51
|
+
resolved_at TEXT,
|
|
52
|
+
evidence_path TEXT
|
|
53
|
+
);
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_db(root: Path) -> sqlite3.Connection:
|
|
58
|
+
db_path = root / ".patchi" / _DB_NAME
|
|
59
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
conn = sqlite3.connect(str(db_path))
|
|
61
|
+
conn.executescript(_CREATE_TABLES)
|
|
62
|
+
# Migrate existing DBs that don't have newer columns
|
|
63
|
+
for col_sql in [
|
|
64
|
+
"ALTER TABLE scan_history ADD COLUMN health_score INTEGER DEFAULT 0",
|
|
65
|
+
"ALTER TABLE scan_history ADD COLUMN metrics TEXT DEFAULT '{}'",
|
|
66
|
+
"ALTER TABLE scan_history ADD COLUMN agent_group TEXT DEFAULT ''",
|
|
67
|
+
]:
|
|
68
|
+
try:
|
|
69
|
+
conn.execute(col_sql)
|
|
70
|
+
conn.commit()
|
|
71
|
+
except sqlite3.OperationalError:
|
|
72
|
+
pass # Column already exists
|
|
73
|
+
return conn
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def patchi_record_scan(
|
|
77
|
+
root: Path,
|
|
78
|
+
tool: str,
|
|
79
|
+
findings: list[dict],
|
|
80
|
+
duration_ms: int = 0,
|
|
81
|
+
health_score: int = 0,
|
|
82
|
+
metrics: dict | None = None,
|
|
83
|
+
agent_group: str = "",
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Record a scan. Returns scan_id."""
|
|
86
|
+
scan_id = uuid.uuid4().hex[:12]
|
|
87
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
88
|
+
|
|
89
|
+
by_sev = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
|
90
|
+
for f in findings:
|
|
91
|
+
sev = f.get("severity", "info")
|
|
92
|
+
by_sev[sev] = by_sev.get(sev, 0) + 1
|
|
93
|
+
|
|
94
|
+
conn = _get_db(root)
|
|
95
|
+
try:
|
|
96
|
+
conn.execute(
|
|
97
|
+
"INSERT INTO scan_history (scan_id, timestamp, repo_path, tool, findings_count, severity_breakdown, duration_ms, health_score, metrics, agent_group) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
98
|
+
(
|
|
99
|
+
scan_id,
|
|
100
|
+
now,
|
|
101
|
+
str(root),
|
|
102
|
+
tool,
|
|
103
|
+
len(findings),
|
|
104
|
+
json.dumps(by_sev),
|
|
105
|
+
duration_ms,
|
|
106
|
+
health_score,
|
|
107
|
+
json.dumps(metrics or {}),
|
|
108
|
+
agent_group,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
for f in findings:
|
|
112
|
+
fid = uuid.uuid4().hex
|
|
113
|
+
conn.execute(
|
|
114
|
+
"INSERT OR REPLACE INTO findings (finding_id, scan_id, rule_id, file, line, severity, status, first_seen) VALUES (?, ?, ?, ?, ?, ?, 'open', ?)",
|
|
115
|
+
(
|
|
116
|
+
fid,
|
|
117
|
+
scan_id,
|
|
118
|
+
f.get("type", ""),
|
|
119
|
+
f.get("file", ""),
|
|
120
|
+
f.get("line", 0),
|
|
121
|
+
f.get("severity", "info"),
|
|
122
|
+
now,
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
conn.commit()
|
|
126
|
+
finally:
|
|
127
|
+
conn.close()
|
|
128
|
+
return scan_id
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def patchi_verify_finding(root: Path, finding_id: str) -> None:
|
|
132
|
+
"""Mark a finding as verified (fix confirmed)."""
|
|
133
|
+
conn = _get_db(root)
|
|
134
|
+
try:
|
|
135
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
136
|
+
conn.execute(
|
|
137
|
+
"UPDATE findings SET status = 'verified', resolved_at = ? WHERE finding_id = ?",
|
|
138
|
+
(now, finding_id),
|
|
139
|
+
)
|
|
140
|
+
conn.commit()
|
|
141
|
+
finally:
|
|
142
|
+
conn.close()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def patchi_get_history(root: Path, limit: int = 20) -> list[dict]:
|
|
146
|
+
"""Get recent scan history."""
|
|
147
|
+
conn = _get_db(root)
|
|
148
|
+
try:
|
|
149
|
+
cursor = conn.execute(
|
|
150
|
+
"SELECT scan_id, timestamp, tool, findings_count, severity_breakdown, duration_ms, health_score FROM scan_history ORDER BY rowid DESC LIMIT ?",
|
|
151
|
+
(limit,),
|
|
152
|
+
)
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
"scan_id": r[0],
|
|
156
|
+
"timestamp": r[1],
|
|
157
|
+
"tool": r[2],
|
|
158
|
+
"findings_count": r[3],
|
|
159
|
+
"severity_breakdown": json.loads(r[4] or "{}"),
|
|
160
|
+
"duration_ms": r[5],
|
|
161
|
+
"health_score": r[6] or 0,
|
|
162
|
+
}
|
|
163
|
+
for r in cursor.fetchall()
|
|
164
|
+
]
|
|
165
|
+
finally:
|
|
166
|
+
conn.close()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def patchi_get_severity_trends(root: Path, days: int = 90) -> dict:
|
|
170
|
+
"""Severity breakdown per day — ready for trend charts.
|
|
171
|
+
|
|
172
|
+
Returns::
|
|
173
|
+
{"labels": ["2025-01-01", ...], "datasets": {"critical": [0, ...], "high": [...], ...}}
|
|
174
|
+
"""
|
|
175
|
+
conn = _get_db(root)
|
|
176
|
+
try:
|
|
177
|
+
rows = conn.execute(
|
|
178
|
+
"""
|
|
179
|
+
SELECT DATE(timestamp) as day, severity_breakdown
|
|
180
|
+
FROM scan_history
|
|
181
|
+
WHERE timestamp >= DATE('now', ?)
|
|
182
|
+
ORDER BY day ASC
|
|
183
|
+
""",
|
|
184
|
+
(f"-{days} days",),
|
|
185
|
+
).fetchall()
|
|
186
|
+
|
|
187
|
+
from collections import defaultdict
|
|
188
|
+
|
|
189
|
+
daily: dict[str, dict[str, int]] = defaultdict(
|
|
190
|
+
lambda: {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
|
191
|
+
)
|
|
192
|
+
for day, sb_json in rows:
|
|
193
|
+
sb = json.loads(sb_json or "{}")
|
|
194
|
+
for sev in ("critical", "high", "medium", "low", "info"):
|
|
195
|
+
daily[day][sev] += sb.get(sev, 0)
|
|
196
|
+
|
|
197
|
+
labels = sorted(daily.keys())
|
|
198
|
+
datasets = {}
|
|
199
|
+
for sev in ("critical", "high", "medium", "low", "info"):
|
|
200
|
+
datasets[sev] = [daily[d][sev] for d in labels]
|
|
201
|
+
return {"labels": labels, "datasets": datasets}
|
|
202
|
+
finally:
|
|
203
|
+
conn.close()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def patchi_get_health_scores(root: Path, days: int = 180) -> dict:
|
|
207
|
+
"""Health score over time for trend charts."""
|
|
208
|
+
conn = _get_db(root)
|
|
209
|
+
try:
|
|
210
|
+
rows = conn.execute(
|
|
211
|
+
"""
|
|
212
|
+
SELECT DATE(timestamp) as day, MAX(health_score) as score
|
|
213
|
+
FROM scan_history
|
|
214
|
+
WHERE timestamp >= DATE('now', ?)
|
|
215
|
+
GROUP BY DATE(timestamp)
|
|
216
|
+
ORDER BY day ASC
|
|
217
|
+
""",
|
|
218
|
+
(f"-{days} days",),
|
|
219
|
+
).fetchall()
|
|
220
|
+
return {"labels": [r[0] for r in rows], "scores": [r[1] for r in rows]}
|
|
221
|
+
finally:
|
|
222
|
+
conn.close()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def patchi_get_metrics_history(root: Path, metric_key: str, days: int = 90) -> dict:
|
|
226
|
+
"""Extract a specific metric (e.g. coverage, bundle_size) over time."""
|
|
227
|
+
conn = _get_db(root)
|
|
228
|
+
try:
|
|
229
|
+
rows = conn.execute(
|
|
230
|
+
"""
|
|
231
|
+
SELECT DATE(timestamp) as day, metrics
|
|
232
|
+
FROM scan_history
|
|
233
|
+
WHERE timestamp >= DATE('now', ?) AND metrics != '{}'
|
|
234
|
+
ORDER BY timestamp ASC
|
|
235
|
+
""",
|
|
236
|
+
(f"-{days} days",),
|
|
237
|
+
).fetchall()
|
|
238
|
+
labels: list[str] = []
|
|
239
|
+
values: list[float] = []
|
|
240
|
+
for day, m_json in rows:
|
|
241
|
+
m = json.loads(m_json or "{}")
|
|
242
|
+
val = m.get(metric_key)
|
|
243
|
+
if val is not None:
|
|
244
|
+
labels.append(day)
|
|
245
|
+
values.append(float(val))
|
|
246
|
+
return {"labels": labels, "values": values}
|
|
247
|
+
finally:
|
|
248
|
+
conn.close()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def patchi_get_analytics(root: Path) -> dict:
|
|
252
|
+
"""Get trend analytics from scan history."""
|
|
253
|
+
conn = _get_db(root)
|
|
254
|
+
try:
|
|
255
|
+
# Total scans
|
|
256
|
+
total = conn.execute("SELECT COUNT(*) FROM scan_history").fetchone()[0]
|
|
257
|
+
|
|
258
|
+
# Findings by status
|
|
259
|
+
status_counts = {}
|
|
260
|
+
for row in conn.execute("SELECT status, COUNT(*) FROM findings GROUP BY status"):
|
|
261
|
+
status_counts[row[0]] = row[1]
|
|
262
|
+
|
|
263
|
+
# Most common rule violations
|
|
264
|
+
common_rules = []
|
|
265
|
+
for row in conn.execute(
|
|
266
|
+
"SELECT rule_id, COUNT(*) as cnt FROM findings WHERE rule_id != '' GROUP BY rule_id ORDER BY cnt DESC LIMIT 10"
|
|
267
|
+
):
|
|
268
|
+
common_rules.append({"rule": row[0], "count": row[1]})
|
|
269
|
+
|
|
270
|
+
# Average findings per scan
|
|
271
|
+
avg_findings = 0
|
|
272
|
+
if total > 0:
|
|
273
|
+
total_findings = (
|
|
274
|
+
conn.execute("SELECT SUM(findings_count) FROM scan_history").fetchone()[0] or 0
|
|
275
|
+
)
|
|
276
|
+
avg_findings = round(total_findings / total, 1)
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"total_scans": total,
|
|
280
|
+
"findings_by_status": status_counts,
|
|
281
|
+
"common_rules": common_rules,
|
|
282
|
+
"avg_findings_per_scan": avg_findings,
|
|
283
|
+
}
|
|
284
|
+
finally:
|
|
285
|
+
conn.close()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ── History Agent ─────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@register
|
|
292
|
+
class HistoryAgent(BaseAgent):
|
|
293
|
+
"""Records scan results to SQLite history and provides analytics."""
|
|
294
|
+
|
|
295
|
+
name = "HistoryAgent"
|
|
296
|
+
group = AgentGroup.GUARD
|
|
297
|
+
timeout = 30
|
|
298
|
+
|
|
299
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
300
|
+
history = patchi_get_history(inp.root, limit=5)
|
|
301
|
+
analytics = patchi_get_analytics(inp.root)
|
|
302
|
+
|
|
303
|
+
result.data["recent_scans"] = history
|
|
304
|
+
result.data["analytics"] = analytics
|
|
305
|
+
|
|
306
|
+
if analytics["total_scans"] == 0:
|
|
307
|
+
result.add_finding(
|
|
308
|
+
Finding(
|
|
309
|
+
agent=self.name,
|
|
310
|
+
type="no_scan_history",
|
|
311
|
+
severity=Severity.INFO,
|
|
312
|
+
file="",
|
|
313
|
+
message="No previous scan history found — this is the first scan",
|
|
314
|
+
)
|
|
315
|
+
)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IaC & Container Security Scanner — Dockerfiles, docker-compose, K8s manifests, Terraform.
|
|
3
|
+
|
|
4
|
+
Detects:
|
|
5
|
+
- Dockerfiles running as root, exposed ports, outdated base images
|
|
6
|
+
- docker-compose privileged containers, host network, volume mounts
|
|
7
|
+
- Kubernetes missing securityContext, resource limits, network policies
|
|
8
|
+
- Terraform public S3, open security groups, unencrypted storage
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
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 IaCScannerAgent(BaseAgent):
|
|
29
|
+
"""Scans IaC files for security misconfigurations."""
|
|
30
|
+
|
|
31
|
+
name = "IaCScannerAgent"
|
|
32
|
+
group = AgentGroup.SECURITY
|
|
33
|
+
timeout = 60
|
|
34
|
+
|
|
35
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
36
|
+
scanned = 0
|
|
37
|
+
|
|
38
|
+
# Dockerfiles
|
|
39
|
+
for fpath in safe_rglob(inp.root, "Dockerfile*"):
|
|
40
|
+
if fpath.is_file():
|
|
41
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
42
|
+
try:
|
|
43
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
44
|
+
except OSError:
|
|
45
|
+
continue
|
|
46
|
+
self._scan_dockerfile(content, rel, result)
|
|
47
|
+
scanned += 1
|
|
48
|
+
|
|
49
|
+
# docker-compose
|
|
50
|
+
for pattern in [
|
|
51
|
+
"docker-compose*.yml",
|
|
52
|
+
"docker-compose*.yaml",
|
|
53
|
+
"compose.yml",
|
|
54
|
+
"compose.yaml",
|
|
55
|
+
]:
|
|
56
|
+
for fpath in safe_rglob(inp.root, pattern):
|
|
57
|
+
if fpath.is_file():
|
|
58
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
59
|
+
try:
|
|
60
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
61
|
+
except OSError:
|
|
62
|
+
continue
|
|
63
|
+
self._scan_docker_compose(content, rel, result)
|
|
64
|
+
scanned += 1
|
|
65
|
+
|
|
66
|
+
# K8s manifests
|
|
67
|
+
for pattern in ["*.yaml", "*.yml"]:
|
|
68
|
+
for fpath in safe_rglob(inp.root, pattern):
|
|
69
|
+
if fpath.is_file():
|
|
70
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
71
|
+
try:
|
|
72
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
73
|
+
except OSError:
|
|
74
|
+
continue
|
|
75
|
+
if "apiVersion:" in content and ("kind:" in content):
|
|
76
|
+
self._scan_k8s(content, rel, result)
|
|
77
|
+
scanned += 1
|
|
78
|
+
|
|
79
|
+
# Terraform
|
|
80
|
+
for fpath in safe_rglob(inp.root, "*.tf"):
|
|
81
|
+
if fpath.is_file():
|
|
82
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
83
|
+
try:
|
|
84
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
85
|
+
except OSError:
|
|
86
|
+
continue
|
|
87
|
+
self._scan_terraform(content, rel, result)
|
|
88
|
+
scanned += 1
|
|
89
|
+
|
|
90
|
+
result.files_scanned = scanned
|
|
91
|
+
|
|
92
|
+
def _scan_dockerfile(self, content: str, file: str, result: AgentResult) -> None:
|
|
93
|
+
lines = content.splitlines()
|
|
94
|
+
has_user = False
|
|
95
|
+
for i, line in enumerate(lines, 1):
|
|
96
|
+
stripped = line.strip()
|
|
97
|
+
if stripped.startswith("#"):
|
|
98
|
+
continue
|
|
99
|
+
if stripped.upper().startswith("USER "):
|
|
100
|
+
has_user = True
|
|
101
|
+
if "root" in stripped.lower():
|
|
102
|
+
result.add_finding(
|
|
103
|
+
Finding(
|
|
104
|
+
agent=self.name,
|
|
105
|
+
type="container_root",
|
|
106
|
+
severity=Severity.HIGH,
|
|
107
|
+
file=file,
|
|
108
|
+
line=i,
|
|
109
|
+
message="Container runs as root",
|
|
110
|
+
cwe="CWE-250",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
if stripped.upper().startswith("EXPOSE "):
|
|
114
|
+
ports = stripped.split()[1:]
|
|
115
|
+
for p in ports:
|
|
116
|
+
if p in ("22", "3389", "6379", "27017", "5432", "3306"):
|
|
117
|
+
result.add_finding(
|
|
118
|
+
Finding(
|
|
119
|
+
agent=self.name,
|
|
120
|
+
type="exposed_sensitive_port",
|
|
121
|
+
severity=Severity.MEDIUM,
|
|
122
|
+
file=file,
|
|
123
|
+
line=i,
|
|
124
|
+
message=f"Sensitive port exposed: {p}",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
if "latest" in stripped.lower() and stripped.upper().startswith("FROM "):
|
|
128
|
+
result.add_finding(
|
|
129
|
+
Finding(
|
|
130
|
+
agent=self.name,
|
|
131
|
+
type="latest_base_image",
|
|
132
|
+
severity=Severity.LOW,
|
|
133
|
+
file=file,
|
|
134
|
+
line=i,
|
|
135
|
+
message="Using 'latest' base image tag — pin to specific version",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
if not has_user and lines:
|
|
139
|
+
result.add_finding(
|
|
140
|
+
Finding(
|
|
141
|
+
agent=self.name,
|
|
142
|
+
type="no_user_directive",
|
|
143
|
+
severity=Severity.MEDIUM,
|
|
144
|
+
file=file,
|
|
145
|
+
message="No USER directive — container defaults to root",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _scan_docker_compose(self, content: str, file: str, result: AgentResult) -> None:
|
|
150
|
+
if "privileged: true" in content:
|
|
151
|
+
result.add_finding(
|
|
152
|
+
Finding(
|
|
153
|
+
agent=self.name,
|
|
154
|
+
type="privileged_container",
|
|
155
|
+
severity=Severity.CRITICAL,
|
|
156
|
+
file=file,
|
|
157
|
+
message="Privileged container detected",
|
|
158
|
+
cwe="CWE-250",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
if "network_mode: host" in content:
|
|
162
|
+
result.add_finding(
|
|
163
|
+
Finding(
|
|
164
|
+
agent=self.name,
|
|
165
|
+
type="host_network",
|
|
166
|
+
severity=Severity.HIGH,
|
|
167
|
+
file=file,
|
|
168
|
+
message="Host network mode — container shares host network stack",
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
if re.search(r'"/etc:/etc"', content) or re.search(r'"/var/run/docker.sock"', content):
|
|
172
|
+
result.add_finding(
|
|
173
|
+
Finding(
|
|
174
|
+
agent=self.name,
|
|
175
|
+
type="dangerous_volume_mount",
|
|
176
|
+
severity=Severity.CRITICAL,
|
|
177
|
+
file=file,
|
|
178
|
+
message="Dangerous volume mount (host system path or Docker socket)",
|
|
179
|
+
cwe="CWE-250",
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _scan_k8s(self, content: str, file: str, result: AgentResult) -> None:
|
|
184
|
+
if "kind: Deployment" in content or "kind: StatefulSet" in content:
|
|
185
|
+
if "securityContext" not in content:
|
|
186
|
+
result.add_finding(
|
|
187
|
+
Finding(
|
|
188
|
+
agent=self.name,
|
|
189
|
+
type="missing_security_context",
|
|
190
|
+
severity=Severity.HIGH,
|
|
191
|
+
file=file,
|
|
192
|
+
message="K8s workload missing securityContext",
|
|
193
|
+
cwe="CWE-250",
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
if "resources:" not in content:
|
|
197
|
+
result.add_finding(
|
|
198
|
+
Finding(
|
|
199
|
+
agent=self.name,
|
|
200
|
+
type="missing_resource_limits",
|
|
201
|
+
severity=Severity.MEDIUM,
|
|
202
|
+
file=file,
|
|
203
|
+
message="K8s workload missing resource limits",
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
if "kind: NetworkPolicy" not in content and "kind: Deployment" in content:
|
|
207
|
+
result.add_finding(
|
|
208
|
+
Finding(
|
|
209
|
+
agent=self.name,
|
|
210
|
+
type="no_network_policy",
|
|
211
|
+
severity=Severity.LOW,
|
|
212
|
+
file=file,
|
|
213
|
+
message="No NetworkPolicy found — all pod-to-pod traffic allowed",
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _scan_terraform(self, content: str, file: str, result: AgentResult) -> None:
|
|
218
|
+
if re.search(r'acl\s*=\s*"public"', content, re.I):
|
|
219
|
+
result.add_finding(
|
|
220
|
+
Finding(
|
|
221
|
+
agent=self.name,
|
|
222
|
+
type="public_s3_bucket",
|
|
223
|
+
severity=Severity.CRITICAL,
|
|
224
|
+
file=file,
|
|
225
|
+
message="S3 bucket with public ACL",
|
|
226
|
+
cwe="CWE-200",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if re.search(r'cidr_blocks\s*=\s*\["0\.0\.0\.0/0"\]', content):
|
|
230
|
+
result.add_finding(
|
|
231
|
+
Finding(
|
|
232
|
+
agent=self.name,
|
|
233
|
+
type="open_security_group",
|
|
234
|
+
severity=Severity.HIGH,
|
|
235
|
+
file=file,
|
|
236
|
+
message="Security group open to 0.0.0.0/0",
|
|
237
|
+
cwe="CWE-284",
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
if re.search(r"encrypt\s*=\s*false", content, re.I):
|
|
241
|
+
result.add_finding(
|
|
242
|
+
Finding(
|
|
243
|
+
agent=self.name,
|
|
244
|
+
type="unencrypted_storage",
|
|
245
|
+
severity=Severity.HIGH,
|
|
246
|
+
file=file,
|
|
247
|
+
message="Unencrypted storage resource",
|
|
248
|
+
cwe="CWE-311",
|
|
249
|
+
)
|
|
250
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ignore Lists with Expiry §10.2.3 — // patchi-ignore: RULE YYYY-MM-DD
|
|
3
|
+
|
|
4
|
+
Parses inline ignore comments with expiry dates, warns on expired ignores.
|
|
5
|
+
Supports: //, #, <!--, /* */ styles across 11 langs.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
findings = filter_ignores(findings, root) # returns (kept, suppressed, expired_warnings)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from datetime import date, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
_log = logging.getLogger("patchi.security.ignore_expiry")
|
|
19
|
+
|
|
20
|
+
# Matches: patchi-ignore, patchi_ignore, patchi:ignore optionally with : RULE and date YYYY-MM-DD or YYYY/MM/DD
|
|
21
|
+
_RE = re.compile(
|
|
22
|
+
r"patchi[\-_:]?ignore\s*:?\s*(?P<rule>[\w\-\*]+)?\s*(?P<date>\d{4}[-/]\d{2}[-/]\d{2})?",
|
|
23
|
+
re.IGNORECASE,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_ignore_line(line: str) -> tuple[str | None, date | None] | None:
|
|
28
|
+
m = _RE.search(line)
|
|
29
|
+
if not m:
|
|
30
|
+
return None
|
|
31
|
+
rule = m.group("rule")
|
|
32
|
+
datestr = m.group("date")
|
|
33
|
+
d = None
|
|
34
|
+
if datestr:
|
|
35
|
+
try:
|
|
36
|
+
d = datetime.strptime(datestr.replace("/", "-"), "%Y-%m-%d").date()
|
|
37
|
+
except ValueError:
|
|
38
|
+
d = None
|
|
39
|
+
# If no rule and no date, still a generic ignore (no expiry)
|
|
40
|
+
if not rule and not d:
|
|
41
|
+
return ("*", None)
|
|
42
|
+
return (rule or "*", d)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_expired(d: date | None) -> bool:
|
|
46
|
+
return d is not None and d < date.today()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def scan_file_ignores(root: Path, rel: str) -> list[dict]:
|
|
50
|
+
"""Return list of {line, rule, expiry, expired} for a file."""
|
|
51
|
+
out: list[dict] = []
|
|
52
|
+
try:
|
|
53
|
+
txt = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
54
|
+
except OSError:
|
|
55
|
+
return out
|
|
56
|
+
for i, line in enumerate(txt.splitlines(), 1):
|
|
57
|
+
parsed = _parse_ignore_line(line)
|
|
58
|
+
if parsed:
|
|
59
|
+
rule, expiry = parsed
|
|
60
|
+
out.append({"line": i, "rule": rule, "expiry": expiry.isoformat() if expiry else None, "expired": _is_expired(expiry)})
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def filter_ignores(findings: list, root: Path) -> tuple[list, list, list]:
|
|
65
|
+
"""
|
|
66
|
+
Filter findings suppressed by patchi-ignore comments.
|
|
67
|
+
Returns (kept, suppressed, expired_warnings).
|
|
68
|
+
|
|
69
|
+
A finding is suppressed if its file has a patchi-ignore comment
|
|
70
|
+
on the same line, previous line, or file-header (line 1) with matching rule or "*"
|
|
71
|
+
and not expired. Expired ignores generate warnings but do NOT suppress.
|
|
72
|
+
"""
|
|
73
|
+
suppressed: list = []
|
|
74
|
+
expired_warnings: list[dict] = []
|
|
75
|
+
# cache per file ignores
|
|
76
|
+
cache: dict[str, list[dict]] = {}
|
|
77
|
+
|
|
78
|
+
def get_ignores(rel: str) -> list[dict]:
|
|
79
|
+
if rel not in cache:
|
|
80
|
+
cache[rel] = scan_file_ignores(root, rel)
|
|
81
|
+
return cache[rel]
|
|
82
|
+
|
|
83
|
+
kept: list = []
|
|
84
|
+
for f in findings:
|
|
85
|
+
rel = getattr(f, "file", "") or (f.get("file", "") if isinstance(f, dict) else "")
|
|
86
|
+
line = getattr(f, "line", 0) or (f.get("line", 0) if isinstance(f, dict) else 0)
|
|
87
|
+
ftype = getattr(f, "type", "") or (f.get("type", "") if isinstance(f, dict) else "")
|
|
88
|
+
if not rel:
|
|
89
|
+
kept.append(f)
|
|
90
|
+
continue
|
|
91
|
+
ignores = get_ignores(rel)
|
|
92
|
+
matched = None
|
|
93
|
+
for ig in ignores:
|
|
94
|
+
# match rule: "*" or exact type or prefix
|
|
95
|
+
rule = ig["rule"]
|
|
96
|
+
if rule != "*" and rule.lower() not in ftype.lower() and ftype.lower() not in rule.lower():
|
|
97
|
+
continue
|
|
98
|
+
# line proximity: same line, previous line, or header
|
|
99
|
+
if ig["line"] in (line, line - 1, 1):
|
|
100
|
+
if ig["expired"]:
|
|
101
|
+
expired_warnings.append({"file": rel, "line": ig["line"], "rule": rule, "finding_type": ftype, "message": f"Expired ignore {rule} expired {ig['expiry']}"})
|
|
102
|
+
continue
|
|
103
|
+
matched = ig
|
|
104
|
+
break
|
|
105
|
+
if matched:
|
|
106
|
+
suppressed.append(f)
|
|
107
|
+
else:
|
|
108
|
+
kept.append(f)
|
|
109
|
+
if expired_warnings:
|
|
110
|
+
_log.warning("ignore expiry: %d expired ignores (not suppressing)", len(expired_warnings))
|
|
111
|
+
return kept, suppressed, expired_warnings
|