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,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anomaly detection for Patchi hosted mode.
|
|
3
|
+
|
|
4
|
+
Two layers:
|
|
5
|
+
1. Statistical: rolling windows — request rate spikes, error rate spikes,
|
|
6
|
+
status code distribution shifts, path enumeration patterns.
|
|
7
|
+
2. ML: IsolationForest on request feature vectors — catches what statistics miss.
|
|
8
|
+
|
|
9
|
+
Detects: brute force, credential stuffing, scanner sweeps, injection probes,
|
|
10
|
+
error spikes, path traversal attempts.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import collections
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
from patchi.core.hosted.log_parsers import LogEntry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AnomalyFinding:
|
|
24
|
+
detector: str
|
|
25
|
+
severity: str # critical / high / medium
|
|
26
|
+
title: str
|
|
27
|
+
detail: str
|
|
28
|
+
ip: str = ""
|
|
29
|
+
evidence: dict = field(default_factory=dict)
|
|
30
|
+
detected_at: float = field(default_factory=time.time)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Statistical detectors ──────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _RollingWindow:
|
|
37
|
+
"""Fixed-size time window for rate tracking."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, window_secs: int = 60) -> None:
|
|
40
|
+
self._window = window_secs
|
|
41
|
+
self._events: collections.deque = collections.deque()
|
|
42
|
+
|
|
43
|
+
def add(self, value: float = 1.0) -> None:
|
|
44
|
+
now = time.time()
|
|
45
|
+
self._events.append((now, value))
|
|
46
|
+
self._trim(now)
|
|
47
|
+
|
|
48
|
+
def _trim(self, now: float) -> None:
|
|
49
|
+
while self._events and now - self._events[0][0] > self._window:
|
|
50
|
+
self._events.popleft()
|
|
51
|
+
|
|
52
|
+
def rate(self) -> float:
|
|
53
|
+
"""Events per second in the current window."""
|
|
54
|
+
if not self._events:
|
|
55
|
+
return 0.0
|
|
56
|
+
self._trim(time.time())
|
|
57
|
+
if not self._events:
|
|
58
|
+
return 0.0
|
|
59
|
+
# Compute actual time span of events in window
|
|
60
|
+
span = self._events[-1][0] - self._events[0][0]
|
|
61
|
+
if span <= 0:
|
|
62
|
+
return float(len(self._events))
|
|
63
|
+
return sum(v for _, v in self._events) / span
|
|
64
|
+
|
|
65
|
+
def count(self) -> int:
|
|
66
|
+
self._trim(time.time())
|
|
67
|
+
return len(self._events)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class StatisticalDetector:
|
|
71
|
+
"""
|
|
72
|
+
Rolling window detectors for common attack patterns.
|
|
73
|
+
Maintains per-IP and global counters.
|
|
74
|
+
Accepts optional config dict for threshold overrides.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# Defaults
|
|
78
|
+
_DEFAULT_RATE_SPIKE_RPS = 50
|
|
79
|
+
_DEFAULT_BRUTE_FORCE_AUTH = 20
|
|
80
|
+
_DEFAULT_SCANNER_PATHS = 30
|
|
81
|
+
_DEFAULT_ERROR_RATE_PCT = 0.40
|
|
82
|
+
_INJECTION_PATTERNS = [
|
|
83
|
+
"../",
|
|
84
|
+
"..\\",
|
|
85
|
+
"%2e%2e",
|
|
86
|
+
"'%20OR%20",
|
|
87
|
+
" OR ",
|
|
88
|
+
"UNION SELECT",
|
|
89
|
+
"<script",
|
|
90
|
+
"javascript:",
|
|
91
|
+
"${jndi:",
|
|
92
|
+
"etc/passwd",
|
|
93
|
+
]
|
|
94
|
+
_MAX_IP_TRACKERS = 10_000
|
|
95
|
+
|
|
96
|
+
def __init__(self, config: dict | None = None) -> None:
|
|
97
|
+
hosted = (config or {}).get("hosted", {})
|
|
98
|
+
self._RATE_SPIKE_RPS = hosted.get("rate_spike_rps", self._DEFAULT_RATE_SPIKE_RPS)
|
|
99
|
+
self._BRUTE_FORCE_AUTH = hosted.get("brute_force_auth", self._DEFAULT_BRUTE_FORCE_AUTH)
|
|
100
|
+
self._SCANNER_PATHS = hosted.get("scanner_paths", self._DEFAULT_SCANNER_PATHS)
|
|
101
|
+
self._ERROR_RATE_PCT = hosted.get("error_rate_pct", self._DEFAULT_ERROR_RATE_PCT)
|
|
102
|
+
self._whitelist: set[str] = set(hosted.get("ip_whitelist", []))
|
|
103
|
+
|
|
104
|
+
self._global_rate = _RollingWindow(60)
|
|
105
|
+
self._error_count = _RollingWindow(60)
|
|
106
|
+
self._total_count = _RollingWindow(60)
|
|
107
|
+
self._ip_auth_hits: dict[str, _RollingWindow] = {}
|
|
108
|
+
self._ip_paths: dict[str, set] = {}
|
|
109
|
+
self._ip_path_times: dict[str, float] = {}
|
|
110
|
+
self._ip_lru_order: list[str] = []
|
|
111
|
+
|
|
112
|
+
def feed(self, entry: LogEntry) -> list[AnomalyFinding]:
|
|
113
|
+
findings: list[AnomalyFinding] = []
|
|
114
|
+
|
|
115
|
+
# Skip whitelisted IPs
|
|
116
|
+
if entry.ip and entry.ip in self._whitelist:
|
|
117
|
+
return findings
|
|
118
|
+
|
|
119
|
+
self._global_rate.add()
|
|
120
|
+
self._total_count.add()
|
|
121
|
+
if entry.status >= 400:
|
|
122
|
+
self._error_count.add()
|
|
123
|
+
|
|
124
|
+
# Rate spike
|
|
125
|
+
if self._global_rate.rate() > self._RATE_SPIKE_RPS:
|
|
126
|
+
findings.append(
|
|
127
|
+
AnomalyFinding(
|
|
128
|
+
detector="rate_spike",
|
|
129
|
+
severity="high",
|
|
130
|
+
title=f"Request rate spike: {self._global_rate.rate():.0f} req/s",
|
|
131
|
+
detail="Sustained high request rate — possible DDoS or scanner.",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Error rate
|
|
136
|
+
total = self._total_count.count()
|
|
137
|
+
if total > 20:
|
|
138
|
+
err_pct = self._error_count.count() / total
|
|
139
|
+
if err_pct > self._ERROR_RATE_PCT:
|
|
140
|
+
findings.append(
|
|
141
|
+
AnomalyFinding(
|
|
142
|
+
detector="error_spike",
|
|
143
|
+
severity="medium",
|
|
144
|
+
title=f"High error rate: {err_pct:.0%}",
|
|
145
|
+
detail=f"{self._error_count.count()} errors in last 60s.",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Per-IP checks
|
|
150
|
+
if entry.ip:
|
|
151
|
+
findings.extend(self._check_ip(entry))
|
|
152
|
+
|
|
153
|
+
# Injection patterns in path
|
|
154
|
+
path_lower = entry.path.lower()
|
|
155
|
+
for pattern in self._INJECTION_PATTERNS:
|
|
156
|
+
if pattern.lower() in path_lower:
|
|
157
|
+
findings.append(
|
|
158
|
+
AnomalyFinding(
|
|
159
|
+
detector="injection_probe",
|
|
160
|
+
severity="critical",
|
|
161
|
+
title=f"Injection probe detected: {pattern}",
|
|
162
|
+
detail=f"IP {entry.ip} sent probe in path: {entry.path[:120]}",
|
|
163
|
+
ip=entry.ip,
|
|
164
|
+
evidence={"path": entry.path, "pattern": pattern},
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return findings
|
|
169
|
+
|
|
170
|
+
def _check_ip(self, entry: LogEntry) -> list[AnomalyFinding]:
|
|
171
|
+
findings: list[AnomalyFinding] = []
|
|
172
|
+
ip = entry.ip
|
|
173
|
+
|
|
174
|
+
# LRU eviction if tracking too many IPs
|
|
175
|
+
if ip not in self._ip_auth_hits and len(self._ip_auth_hits) >= self._MAX_IP_TRACKERS:
|
|
176
|
+
self._evict_lru()
|
|
177
|
+
|
|
178
|
+
# Brute force on auth endpoints
|
|
179
|
+
auth_paths = ("/auth/login", "/login", "/signin", "/api/token", "/admin")
|
|
180
|
+
if any(entry.path.startswith(p) for p in auth_paths) and entry.method == "POST":
|
|
181
|
+
if ip not in self._ip_auth_hits:
|
|
182
|
+
self._ip_auth_hits[ip] = _RollingWindow(60)
|
|
183
|
+
self._ip_lru_order.append(ip)
|
|
184
|
+
self._ip_auth_hits[ip].add()
|
|
185
|
+
if self._ip_auth_hits[ip].count() >= self._BRUTE_FORCE_AUTH:
|
|
186
|
+
findings.append(
|
|
187
|
+
AnomalyFinding(
|
|
188
|
+
detector="brute_force",
|
|
189
|
+
severity="critical",
|
|
190
|
+
title=f"Brute force: {ip} → {entry.path}",
|
|
191
|
+
detail=f"{self._ip_auth_hits[ip].count()} auth attempts in 60s.",
|
|
192
|
+
ip=ip,
|
|
193
|
+
evidence={"count": self._ip_auth_hits[ip].count()},
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Path enumeration (scanner sweep)
|
|
198
|
+
now = time.time()
|
|
199
|
+
last_reset = self._ip_path_times.get(ip, 0)
|
|
200
|
+
if now - last_reset > 60:
|
|
201
|
+
self._ip_paths[ip] = set()
|
|
202
|
+
self._ip_path_times[ip] = now
|
|
203
|
+
if ip not in self._ip_lru_order:
|
|
204
|
+
self._ip_lru_order.append(ip)
|
|
205
|
+
self._ip_paths[ip].add(entry.path)
|
|
206
|
+
if len(self._ip_paths[ip]) >= self._SCANNER_PATHS:
|
|
207
|
+
findings.append(
|
|
208
|
+
AnomalyFinding(
|
|
209
|
+
detector="scanner_sweep",
|
|
210
|
+
severity="high",
|
|
211
|
+
title=f"Scanner sweep: {ip} probing {len(self._ip_paths[ip])} paths",
|
|
212
|
+
detail="Rapid path enumeration — possible automated scanner.",
|
|
213
|
+
ip=ip,
|
|
214
|
+
evidence={"distinct_paths": len(self._ip_paths[ip])},
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return findings
|
|
219
|
+
|
|
220
|
+
def _evict_lru(self) -> None:
|
|
221
|
+
"""Evict oldest tracked IPs to bound memory usage."""
|
|
222
|
+
while len(self._ip_auth_hits) >= self._MAX_IP_TRACKERS and self._ip_lru_order:
|
|
223
|
+
old_ip = self._ip_lru_order.pop(0)
|
|
224
|
+
self._ip_auth_hits.pop(old_ip, None)
|
|
225
|
+
self._ip_paths.pop(old_ip, None)
|
|
226
|
+
self._ip_path_times.pop(old_ip, None)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class MLDetector:
|
|
230
|
+
"""
|
|
231
|
+
IsolationForest anomaly detector on request feature vectors.
|
|
232
|
+
Trains on a baseline window, then flags outliers.
|
|
233
|
+
Requires sklearn — gracefully no-ops if not available.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
MIN_SAMPLES = 100 # minimum before ML kicks in
|
|
237
|
+
_MAX_SAMPLES = 10_000 # cap memory usage
|
|
238
|
+
|
|
239
|
+
def __init__(self) -> None:
|
|
240
|
+
self._samples: list[list[float]] = []
|
|
241
|
+
self._model = None
|
|
242
|
+
self._trained = False
|
|
243
|
+
|
|
244
|
+
def _to_vector(self, entry: LogEntry) -> list[float]:
|
|
245
|
+
"""Convert a log entry to a numeric feature vector."""
|
|
246
|
+
return [
|
|
247
|
+
float(entry.status),
|
|
248
|
+
float(len(entry.path)),
|
|
249
|
+
float(entry.path.count("/")),
|
|
250
|
+
float(1 if entry.method == "POST" else 0),
|
|
251
|
+
float(1 if entry.status >= 500 else 0),
|
|
252
|
+
float(1 if entry.status == 404 else 0),
|
|
253
|
+
float(len(entry.path.split("?")[1]) if "?" in entry.path else 0),
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
def feed(self, entry: LogEntry) -> list[AnomalyFinding]:
|
|
257
|
+
try:
|
|
258
|
+
from sklearn.ensemble import IsolationForest
|
|
259
|
+
except ImportError:
|
|
260
|
+
return []
|
|
261
|
+
|
|
262
|
+
vec = self._to_vector(entry)
|
|
263
|
+
self._samples.append(vec)
|
|
264
|
+
|
|
265
|
+
# Cap memory usage
|
|
266
|
+
if len(self._samples) > self._MAX_SAMPLES:
|
|
267
|
+
self._samples = self._samples[-self._MAX_SAMPLES :]
|
|
268
|
+
|
|
269
|
+
if len(self._samples) < self.MIN_SAMPLES:
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
# Retrain every 500 samples
|
|
273
|
+
if len(self._samples) % 500 == 0 or not self._trained:
|
|
274
|
+
self._model = IsolationForest(contamination=0.05, random_state=42, n_estimators=50)
|
|
275
|
+
self._model.fit(self._samples[-1000:])
|
|
276
|
+
self._trained = True
|
|
277
|
+
|
|
278
|
+
if not self._model:
|
|
279
|
+
return []
|
|
280
|
+
|
|
281
|
+
score = self._model.decision_function([vec])[0]
|
|
282
|
+
pred = self._model.predict([vec])[0]
|
|
283
|
+
|
|
284
|
+
if pred == -1 and score < -0.2:
|
|
285
|
+
return [
|
|
286
|
+
AnomalyFinding(
|
|
287
|
+
detector="ml_outlier",
|
|
288
|
+
severity="medium",
|
|
289
|
+
title="ML anomaly detected",
|
|
290
|
+
detail=(
|
|
291
|
+
f"Request to {entry.path} from {entry.ip} has anomaly score {score:.3f}."
|
|
292
|
+
),
|
|
293
|
+
ip=entry.ip,
|
|
294
|
+
evidence={"score": round(score, 3), "status": entry.status},
|
|
295
|
+
)
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
return []
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rotating audit log for Patchi hosted mode.
|
|
3
|
+
|
|
4
|
+
Writes structured JSON-lines to .patchi/hosted/audit.log.
|
|
5
|
+
Rotates when file exceeds MAX_SIZE_BYTES, keeping MAX_BACKUPS old files.
|
|
6
|
+
Each entry: {timestamp, event, actor, data}.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_LOG_FILE = ".patchi/hosted/audit.log"
|
|
16
|
+
MAX_SIZE_BYTES = 5 * 1024 * 1024 # 5 MB
|
|
17
|
+
MAX_BACKUPS = 3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_log = logging.getLogger("patchi.core.audit_log")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _log_path(root: Path) -> Path:
|
|
24
|
+
return root / _LOG_FILE
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _rotate(path: Path) -> None:
|
|
28
|
+
"""Rotate old log files, keeping MAX_BACKUPS copies."""
|
|
29
|
+
# Shift existing backups down (remove oldest first to avoid conflicts)
|
|
30
|
+
for i in range(MAX_BACKUPS - 1, 0, -1):
|
|
31
|
+
src = Path(f"{path}.{i}")
|
|
32
|
+
dst = Path(f"{path}.{i + 1}")
|
|
33
|
+
if dst.exists():
|
|
34
|
+
dst.unlink()
|
|
35
|
+
if src.exists():
|
|
36
|
+
src.rename(dst)
|
|
37
|
+
# Move current log to .1
|
|
38
|
+
if path.exists():
|
|
39
|
+
dst = Path(f"{path}.1")
|
|
40
|
+
if dst.exists():
|
|
41
|
+
dst.unlink()
|
|
42
|
+
path.rename(dst)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def write(root: Path, event: str, actor: str = "patchi", data: dict | None = None) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Append one audit entry. Rotates if log exceeds MAX_SIZE_BYTES.
|
|
48
|
+
Never raises — audit logging must not crash the caller.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
path = _log_path(root)
|
|
52
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
if path.exists() and path.stat().st_size >= MAX_SIZE_BYTES:
|
|
55
|
+
_rotate(path)
|
|
56
|
+
|
|
57
|
+
entry = json.dumps(
|
|
58
|
+
{
|
|
59
|
+
"timestamp": time.time(),
|
|
60
|
+
"event": event,
|
|
61
|
+
"actor": actor,
|
|
62
|
+
"data": data or {},
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
with path.open("a", encoding="utf-8") as f:
|
|
66
|
+
f.write(entry + "\n")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
_log.warning("write failed: %s", e)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def read_recent(root: Path, limit: int = 50) -> list[dict]:
|
|
72
|
+
"""Return the most recent `limit` audit entries, newest first."""
|
|
73
|
+
path = _log_path(root)
|
|
74
|
+
if not path.exists():
|
|
75
|
+
return []
|
|
76
|
+
try:
|
|
77
|
+
lines = path.read_text(encoding="utf-8").strip().splitlines()
|
|
78
|
+
entries = []
|
|
79
|
+
for line in reversed(lines[-limit * 2 :]):
|
|
80
|
+
try:
|
|
81
|
+
entries.append(json.loads(line))
|
|
82
|
+
if len(entries) >= limit:
|
|
83
|
+
break
|
|
84
|
+
except json.JSONDecodeError:
|
|
85
|
+
continue
|
|
86
|
+
return entries
|
|
87
|
+
except OSError:
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def clear(root: Path) -> int:
|
|
92
|
+
"""Delete current log and all backups. Returns number of files removed."""
|
|
93
|
+
path = _log_path(root)
|
|
94
|
+
removed = 0
|
|
95
|
+
for p in [path] + [Path(f"{path}.{i}") for i in range(1, MAX_BACKUPS + 2)]:
|
|
96
|
+
try:
|
|
97
|
+
if p.exists():
|
|
98
|
+
p.unlink()
|
|
99
|
+
removed += 1
|
|
100
|
+
except OSError:
|
|
101
|
+
pass
|
|
102
|
+
return removed
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compliance Report Generator — Evidence packs for hosted mode.
|
|
3
|
+
|
|
4
|
+
Aggregates scan results, health score, audit log, and domain control status
|
|
5
|
+
into a single structured report suitable for auditors / customers.
|
|
6
|
+
Deterministic: built entirely from data already on disk (no AI required).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_log = logging.getLogger("patchi.hosted.compliance")
|
|
16
|
+
|
|
17
|
+
# Map Patchi security agents to compliance framework domains
|
|
18
|
+
_FRAMEWORK_MAPS: dict[str, dict[str, list[str]]] = {
|
|
19
|
+
"owasp-asvs": {
|
|
20
|
+
"V1: Architecture": ["AppMapperAgent", "BlastRadiusAgent", "PlanAuditorAgent"],
|
|
21
|
+
"V2: Authentication": [
|
|
22
|
+
"AuthenticationAuditAgent",
|
|
23
|
+
"JWTSecurityAgent",
|
|
24
|
+
"SessionManagementAgent",
|
|
25
|
+
"SecretScanner",
|
|
26
|
+
],
|
|
27
|
+
"V3: Session Management": ["SessionManagementAgent", "JWTSecurityAgent"],
|
|
28
|
+
"V4: Access Control": ["AuthZAgent", "InjectionAgent"],
|
|
29
|
+
"V5: Validation & Encoding": ["InjectionAgent", "TaintAnalyzer", "SensitiveDataAgent"],
|
|
30
|
+
"V6: Stored Cryptography": ["CryptoAgent", "SecretsGuard"],
|
|
31
|
+
"V7: Error Handling & Logging": ["HistoryAgent", "GovernanceAgent"],
|
|
32
|
+
"V8: Data Protection": ["PrivacyAgent", "SensitiveDataAgent"],
|
|
33
|
+
"V9: Communications": ["NetworkAgent", "SSRFProtectionAgent", "CORSAuditor"],
|
|
34
|
+
"V10: Malicious Code": ["SupplyChainAgent", "DependencyVulnerabilityAgent"],
|
|
35
|
+
"V12: Files & Resources": ["MisconfigAgent", "ConfigAuditAgent"],
|
|
36
|
+
"V14: Configuration": [
|
|
37
|
+
"ConfigAuditAgent",
|
|
38
|
+
"HeaderAuditAgent",
|
|
39
|
+
"RateLimitAuditor",
|
|
40
|
+
"EnvVarValidator",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
"pci-dss": {
|
|
44
|
+
"Req 2: Secure Configs": ["ConfigAuditAgent", "MisconfigAgent", "HeaderAuditAgent"],
|
|
45
|
+
"Req 3: Protect Stored Data": ["SecretScanner", "SensitiveDataAgent", "CryptoAgent"],
|
|
46
|
+
"Req 4: Encrypt Transmission": ["NetworkAgent", "CryptoAgent"],
|
|
47
|
+
"Req 5: Malware Protection": ["SupplyChainAgent"],
|
|
48
|
+
"Req 6: Secure Development": [
|
|
49
|
+
"TaintAnalyzer",
|
|
50
|
+
"InjectionAgent",
|
|
51
|
+
"DependencyVulnerabilityAgent",
|
|
52
|
+
],
|
|
53
|
+
"Req 8: Identify & Auth": ["AuthenticationAuditAgent", "JWTSecurityAgent"],
|
|
54
|
+
"Req 10: Log & Monitor": ["HistoryAgent", "GovernanceAgent", "EnvVarValidator"],
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def generate_report(root: Path, standard: str = "owasp-asvs") -> dict:
|
|
60
|
+
"""Build a structured compliance evidence report from disk state.
|
|
61
|
+
|
|
62
|
+
Returns {standard, generated_at, controls: [...], summary: {...}} where
|
|
63
|
+
each control row carries pass/fail/pending status plus the agent evidence
|
|
64
|
+
behind it. Never raises — missing data becomes 'pending'.
|
|
65
|
+
"""
|
|
66
|
+
from patchi.core import memory as mem
|
|
67
|
+
|
|
68
|
+
framework = _FRAMEWORK_MAPS.get(standard)
|
|
69
|
+
if framework is None:
|
|
70
|
+
return {"error": f"Unknown standard '{standard}'. Supported: {', '.join(_FRAMEWORK_MAPS)}"}
|
|
71
|
+
|
|
72
|
+
scans = mem.get_scan_results(root)
|
|
73
|
+
brain = mem.get_brain(root)
|
|
74
|
+
health = brain.get("health_score", {})
|
|
75
|
+
|
|
76
|
+
def _agent_findings(agent: str) -> list[dict]:
|
|
77
|
+
return [f for f in scans.get(agent, {}).get("findings", []) if isinstance(f, dict)]
|
|
78
|
+
|
|
79
|
+
def _severity_counts(findings: list[dict]) -> dict[str, int]:
|
|
80
|
+
counts: dict[str, int] = {}
|
|
81
|
+
for f in findings:
|
|
82
|
+
sev = f.get("severity", "info")
|
|
83
|
+
counts[sev] = counts.get(sev, 0) + 1
|
|
84
|
+
return counts
|
|
85
|
+
|
|
86
|
+
sections: list[dict] = []
|
|
87
|
+
total_pass = total_fail = total_pending = 0
|
|
88
|
+
|
|
89
|
+
for section_name, agents in framework.items():
|
|
90
|
+
rows = []
|
|
91
|
+
for agent in agents:
|
|
92
|
+
findings = _agent_findings(agent)
|
|
93
|
+
counts = _severity_counts(findings)
|
|
94
|
+
|
|
95
|
+
if agent not in scans:
|
|
96
|
+
status, evidence = "pending", f"{agent} has not run yet"
|
|
97
|
+
elif counts.get("critical", 0) or counts.get("high", 0):
|
|
98
|
+
status = "fail"
|
|
99
|
+
evidence = (
|
|
100
|
+
f"{counts.get('critical', 0)} critical / {counts.get('high', 0)} high "
|
|
101
|
+
f"finding(s) from {agent}"
|
|
102
|
+
)
|
|
103
|
+
elif findings:
|
|
104
|
+
status = "pass_with_notes"
|
|
105
|
+
evidence = f"{len(findings)} low/medium finding(s) from {agent}"
|
|
106
|
+
else:
|
|
107
|
+
status = "pass"
|
|
108
|
+
evidence = f"{agent} ran clean"
|
|
109
|
+
|
|
110
|
+
if status == "fail":
|
|
111
|
+
total_fail += 1
|
|
112
|
+
elif status == "pending":
|
|
113
|
+
total_pending += 1
|
|
114
|
+
else:
|
|
115
|
+
total_pass += 1
|
|
116
|
+
|
|
117
|
+
rows.append({"control_agent": agent, "status": status, "evidence": evidence})
|
|
118
|
+
|
|
119
|
+
sections.append(
|
|
120
|
+
{
|
|
121
|
+
"section": section_name,
|
|
122
|
+
"controls": rows,
|
|
123
|
+
"failed": sum(1 for r in rows if r["status"] == "fail"),
|
|
124
|
+
"total": len(rows),
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"standard": standard,
|
|
130
|
+
"generated_at": datetime.now(UTC).isoformat(),
|
|
131
|
+
"project": {
|
|
132
|
+
"purpose": brain.get("project_purpose", ""),
|
|
133
|
+
"domain": brain.get("project_domain", ""),
|
|
134
|
+
"framework": brain.get("framework", ""),
|
|
135
|
+
"file_count": brain.get("file_count", 0),
|
|
136
|
+
},
|
|
137
|
+
"health_score": health.get("total"),
|
|
138
|
+
"summary": {
|
|
139
|
+
"sections": len(sections),
|
|
140
|
+
"controls_total": sum(s["total"] for s in sections),
|
|
141
|
+
"controls_failed": total_fail,
|
|
142
|
+
"controls_passed": total_pass,
|
|
143
|
+
"controls_pending": total_pending,
|
|
144
|
+
# Simple posture rollout: pass ratio over evaluated controls
|
|
145
|
+
"posture_pct": round(total_pass / max(1, total_pass + total_fail) * 100, 1),
|
|
146
|
+
},
|
|
147
|
+
"sections": sections,
|
|
148
|
+
}
|