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,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NetworkAgent — network security configuration issues.
|
|
3
|
+
|
|
4
|
+
Detects network-related security issues:
|
|
5
|
+
- Insecure SSL/TLS configurations
|
|
6
|
+
- Missing HTTPS redirects
|
|
7
|
+
- Weak cipher suites
|
|
8
|
+
- Open ports/services
|
|
9
|
+
- Missing security headers
|
|
10
|
+
- Improper firewall rules
|
|
11
|
+
- Unencrypted communication
|
|
12
|
+
|
|
13
|
+
Uses pattern matching and configuration analysis.
|
|
14
|
+
Does NOT call AI.
|
|
15
|
+
Does NOT write to disk.
|
|
16
|
+
Does NOT touch the queue.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from ..agents.base import (
|
|
25
|
+
AgentDomain,
|
|
26
|
+
AgentGroup,
|
|
27
|
+
AgentInput,
|
|
28
|
+
AgentResult,
|
|
29
|
+
AgentStatus,
|
|
30
|
+
BaseAgent,
|
|
31
|
+
Finding,
|
|
32
|
+
Severity,
|
|
33
|
+
make_finding,
|
|
34
|
+
register,
|
|
35
|
+
safe_rglob,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@register
|
|
40
|
+
class NetworkAgent(BaseAgent):
|
|
41
|
+
"""Agent for detecting network security issues."""
|
|
42
|
+
|
|
43
|
+
group = AgentGroup.SECURITY
|
|
44
|
+
domain = AgentDomain.SECURITY
|
|
45
|
+
name = "NetworkAgent"
|
|
46
|
+
description = "Network security: SSL/TLS, HTTPS redirects, cipher suites, security headers"
|
|
47
|
+
|
|
48
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
49
|
+
"""Run network security configuration detection."""
|
|
50
|
+
findings = []
|
|
51
|
+
|
|
52
|
+
# Define configuration and source file patterns to scan
|
|
53
|
+
patterns = [
|
|
54
|
+
"*.py",
|
|
55
|
+
"*.js",
|
|
56
|
+
"*.jsx",
|
|
57
|
+
"*.ts",
|
|
58
|
+
"*.tsx",
|
|
59
|
+
"*.java",
|
|
60
|
+
"*.php",
|
|
61
|
+
"*.rb",
|
|
62
|
+
"*.go",
|
|
63
|
+
"*.rs",
|
|
64
|
+
"*.cpp",
|
|
65
|
+
"*.cxx",
|
|
66
|
+
"*.cc",
|
|
67
|
+
"*.c",
|
|
68
|
+
"*.h",
|
|
69
|
+
"*.hpp",
|
|
70
|
+
"*.cs",
|
|
71
|
+
"*.json",
|
|
72
|
+
"*.yml",
|
|
73
|
+
"*.yaml",
|
|
74
|
+
"*.toml",
|
|
75
|
+
"*.config",
|
|
76
|
+
"*.conf",
|
|
77
|
+
"*.ini",
|
|
78
|
+
"*.xml",
|
|
79
|
+
"Dockerfile*",
|
|
80
|
+
"docker-compose*.yml",
|
|
81
|
+
"**/config/**",
|
|
82
|
+
"**/nginx/**",
|
|
83
|
+
"**/apache/**",
|
|
84
|
+
"**/ssl/**",
|
|
85
|
+
"**/tls/**",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Search for files
|
|
89
|
+
for pattern in patterns:
|
|
90
|
+
for file_path in safe_rglob(inp.root, pattern):
|
|
91
|
+
if file_path.is_file():
|
|
92
|
+
rel_path = file_path.relative_to(inp.root).as_posix()
|
|
93
|
+
if not self._should_skip_file(rel_path, inp):
|
|
94
|
+
findings.extend(self._scan_file_network_security(file_path, rel_path))
|
|
95
|
+
|
|
96
|
+
result.status = AgentStatus.SUCCEEDED
|
|
97
|
+
result.findings = findings
|
|
98
|
+
result.data.update(
|
|
99
|
+
{
|
|
100
|
+
"network_findings": len(
|
|
101
|
+
[
|
|
102
|
+
f
|
|
103
|
+
for f in findings
|
|
104
|
+
if any(
|
|
105
|
+
word in f.title.lower()
|
|
106
|
+
for word in [
|
|
107
|
+
"ssl",
|
|
108
|
+
"tls",
|
|
109
|
+
"https",
|
|
110
|
+
"cipher",
|
|
111
|
+
"header",
|
|
112
|
+
"redirect",
|
|
113
|
+
"encryption",
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
]
|
|
117
|
+
),
|
|
118
|
+
"needs_ai": False,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
|
|
124
|
+
"""Check if file should be skipped based on restrictions."""
|
|
125
|
+
from pathlib import PurePosixPath
|
|
126
|
+
|
|
127
|
+
# Check restrictions
|
|
128
|
+
restrictions = inp.config.get("restrictions", [])
|
|
129
|
+
for r in restrictions:
|
|
130
|
+
if r.get("enabled", True):
|
|
131
|
+
path = r["path"]
|
|
132
|
+
if file_path.startswith(path) or PurePosixPath(file_path).match(path):
|
|
133
|
+
if r["type"] == "NO_TOUCH":
|
|
134
|
+
return True
|
|
135
|
+
elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
|
|
136
|
+
return True
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def _scan_file_network_security(self, file_path: Path, rel_path: str) -> list[Finding]:
|
|
140
|
+
"""Scan a file for network security issues."""
|
|
141
|
+
findings = []
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
content = file_path.read_text(encoding="utf-8")
|
|
145
|
+
|
|
146
|
+
# Check for SSL/TLS issues
|
|
147
|
+
findings.extend(self._scan_ssl_tls_issues(content, rel_path))
|
|
148
|
+
|
|
149
|
+
# Check for HTTPS redirect issues
|
|
150
|
+
findings.extend(self._scan_https_redirects(content, rel_path))
|
|
151
|
+
|
|
152
|
+
# Check for weak cipher suites
|
|
153
|
+
findings.extend(self._scan_cipher_suites(content, rel_path))
|
|
154
|
+
|
|
155
|
+
# Check for missing security headers
|
|
156
|
+
findings.extend(self._scan_security_headers(content, rel_path))
|
|
157
|
+
|
|
158
|
+
# Check for other network security issues
|
|
159
|
+
findings.extend(self._scan_other_network_issues(content, rel_path))
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
findings.append(
|
|
163
|
+
make_finding(
|
|
164
|
+
severity=Severity.LOW,
|
|
165
|
+
file=rel_path,
|
|
166
|
+
line_start=0,
|
|
167
|
+
title="Network security scanner file read error",
|
|
168
|
+
description=f"Could not analyze {file_path.name} for network security issues: {str(e)}",
|
|
169
|
+
evidence=str(e),
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return findings
|
|
174
|
+
|
|
175
|
+
def _scan_ssl_tls_issues(self, content: str, rel_path: str) -> list[Finding]:
|
|
176
|
+
"""Scan for SSL/TLS configuration issues."""
|
|
177
|
+
findings = []
|
|
178
|
+
|
|
179
|
+
# Look for weak SSL/TLS protocol versions
|
|
180
|
+
ssl_patterns = [
|
|
181
|
+
(r"SSLv2|SSLv3|TLSv1(\.0)?", "Weak SSL/TLS Protocol", Severity.CRITICAL),
|
|
182
|
+
(r"ssl_version.*TLSv1(_0|0)?", "Weak TLS Version", Severity.CRITICAL),
|
|
183
|
+
(r"tls_min_version.*TLSv1(_0|0)?", "Weak TLS Minimum Version", Severity.CRITICAL),
|
|
184
|
+
(
|
|
185
|
+
r"protocol.*SSLv2|protocol.*SSLv3|protocol.*TLSv1(\.0)?",
|
|
186
|
+
"Weak Protocol Configuration",
|
|
187
|
+
Severity.CRITICAL,
|
|
188
|
+
),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
lines = content.splitlines()
|
|
192
|
+
for i, line in enumerate(lines, 1):
|
|
193
|
+
for pattern, description, severity in ssl_patterns:
|
|
194
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
195
|
+
for match in matches:
|
|
196
|
+
findings.append(
|
|
197
|
+
make_finding(
|
|
198
|
+
severity=severity,
|
|
199
|
+
file=rel_path,
|
|
200
|
+
line_start=i,
|
|
201
|
+
title=description,
|
|
202
|
+
description=f"Weak SSL/TLS protocol configuration found: {match.group(0)}",
|
|
203
|
+
evidence=line.strip(),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return findings
|
|
208
|
+
|
|
209
|
+
def _scan_https_redirects(self, content: str, rel_path: str) -> list[Finding]:
|
|
210
|
+
"""Scan for HTTPS redirect issues."""
|
|
211
|
+
findings = []
|
|
212
|
+
|
|
213
|
+
has_https_enforcement = bool(
|
|
214
|
+
re.search(
|
|
215
|
+
r"redirect.*https|enable.*https|hsts|strict-transport-security",
|
|
216
|
+
content,
|
|
217
|
+
re.IGNORECASE,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
has_plain_http = bool(
|
|
221
|
+
re.search(
|
|
222
|
+
r"http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)",
|
|
223
|
+
content,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if has_plain_http and not has_https_enforcement:
|
|
228
|
+
lines = content.splitlines()
|
|
229
|
+
for i, line in enumerate(lines, 1):
|
|
230
|
+
if re.search(r"http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)", line):
|
|
231
|
+
findings.append(
|
|
232
|
+
make_finding(
|
|
233
|
+
severity=Severity.MEDIUM,
|
|
234
|
+
file=rel_path,
|
|
235
|
+
line_start=i,
|
|
236
|
+
title="Plain HTTP without HTTPS enforcement",
|
|
237
|
+
description="Plain HTTP URL found without HTTPS redirect/enforcement",
|
|
238
|
+
evidence=line.strip(),
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return findings
|
|
243
|
+
|
|
244
|
+
def _scan_cipher_suites(self, content: str, rel_path: str) -> list[Finding]:
|
|
245
|
+
"""Scan for weak cipher suite configurations."""
|
|
246
|
+
findings = []
|
|
247
|
+
|
|
248
|
+
# Look for weak cipher suites
|
|
249
|
+
weak_cipher_patterns = [
|
|
250
|
+
(r"\bRC4\b|\bDES\b|\b3DES\b|\bMD5\b|\bSHA1\b", "Weak Cipher Suite", Severity.HIGH),
|
|
251
|
+
(
|
|
252
|
+
r"cipher.*\bRC4\b|cipher.*\bDES\b|cipher.*\b3DES\b|cipher.*\bMD5\b",
|
|
253
|
+
"Weak Cipher Configuration",
|
|
254
|
+
Severity.HIGH,
|
|
255
|
+
),
|
|
256
|
+
(r"\baes-128\b|\bdes-cbc\b", "Potentially Weak Cipher", Severity.MEDIUM),
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
lines = content.splitlines()
|
|
260
|
+
for i, line in enumerate(lines, 1):
|
|
261
|
+
for pattern, description, severity in weak_cipher_patterns:
|
|
262
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
263
|
+
for match in matches:
|
|
264
|
+
findings.append(
|
|
265
|
+
make_finding(
|
|
266
|
+
severity=severity,
|
|
267
|
+
file=rel_path,
|
|
268
|
+
line_start=i,
|
|
269
|
+
title=description,
|
|
270
|
+
description=f"Weak cipher suite configuration found: {match.group(0)}",
|
|
271
|
+
evidence=line.strip(),
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return findings
|
|
276
|
+
|
|
277
|
+
def _scan_security_headers(self, content: str, rel_path: str) -> list[Finding]:
|
|
278
|
+
"""Scan for missing security headers."""
|
|
279
|
+
findings = []
|
|
280
|
+
|
|
281
|
+
# Look for missing security headers
|
|
282
|
+
security_headers = [
|
|
283
|
+
"Strict-Transport-Security",
|
|
284
|
+
"X-Frame-Options",
|
|
285
|
+
"X-Content-Type-Options",
|
|
286
|
+
"X-XSS-Protection",
|
|
287
|
+
"Content-Security-Policy",
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
# Check if security headers are set
|
|
291
|
+
lines = content.splitlines()
|
|
292
|
+
for _i, line in enumerate(lines, 1):
|
|
293
|
+
# Look for response header setting
|
|
294
|
+
if any(header.lower() in line.lower() for header in security_headers):
|
|
295
|
+
continue # Header is being set, no issue here
|
|
296
|
+
elif any(
|
|
297
|
+
setting_pattern in line.lower()
|
|
298
|
+
for setting_pattern in ["header(", "setheader", "response.header"]
|
|
299
|
+
):
|
|
300
|
+
# Check if it's setting a security header
|
|
301
|
+
has_security_header = False
|
|
302
|
+
for header in security_headers:
|
|
303
|
+
if header.lower() in line.lower():
|
|
304
|
+
has_security_header = True
|
|
305
|
+
break
|
|
306
|
+
if not has_security_header:
|
|
307
|
+
# This line sets a header but not a security header
|
|
308
|
+
# This alone isn't necessarily bad, but let's check for missing headers in context
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
# Look for common web frameworks where security headers should be set
|
|
312
|
+
framework_indicators = ["express", "django", "flask", "fastapi", "spring", "laravel"]
|
|
313
|
+
if any(indicator in content.lower() for indicator in framework_indicators):
|
|
314
|
+
# Check if security headers are configured
|
|
315
|
+
has_hsts = any(
|
|
316
|
+
hsts_pattern in content.lower()
|
|
317
|
+
for hsts_pattern in ["strict-transport-security", "hsts"]
|
|
318
|
+
)
|
|
319
|
+
has_xfo = any(
|
|
320
|
+
xfo_pattern in content.lower()
|
|
321
|
+
for xfo_pattern in ["x-frame-options", "frame-options"]
|
|
322
|
+
)
|
|
323
|
+
has_xcto = any(
|
|
324
|
+
xcto_pattern in content.lower()
|
|
325
|
+
for xcto_pattern in ["x-content-type-options", "nosniff"]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
missing_headers = []
|
|
329
|
+
if not has_hsts:
|
|
330
|
+
missing_headers.append("HSTS")
|
|
331
|
+
if not has_xfo:
|
|
332
|
+
missing_headers.append("X-Frame-Options")
|
|
333
|
+
if not has_xcto:
|
|
334
|
+
missing_headers.append("X-Content-Type-Options")
|
|
335
|
+
|
|
336
|
+
if missing_headers:
|
|
337
|
+
findings.append(
|
|
338
|
+
make_finding(
|
|
339
|
+
severity=Severity.MEDIUM,
|
|
340
|
+
file=rel_path,
|
|
341
|
+
line_start=0,
|
|
342
|
+
title=f"Missing Security Headers: {', '.join(missing_headers)}",
|
|
343
|
+
description=f"Common security headers not configured: {', '.join(missing_headers)}",
|
|
344
|
+
evidence=f"Detected web framework but missing security headers: {', '.join(missing_headers)}",
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return findings
|
|
349
|
+
|
|
350
|
+
def _scan_other_network_issues(self, content: str, rel_path: str) -> list[Finding]:
|
|
351
|
+
"""Scan for other network security issues."""
|
|
352
|
+
findings = []
|
|
353
|
+
|
|
354
|
+
# Look for plain HTTP URLs in secure contexts
|
|
355
|
+
http_patterns = [
|
|
356
|
+
(r'http://[^s][^"\'\s]+', "Plain HTTP URL", Severity.MEDIUM),
|
|
357
|
+
(r'"http://|\'http://', "Plain HTTP Reference", Severity.MEDIUM),
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
lines = content.splitlines()
|
|
361
|
+
for i, line in enumerate(lines, 1):
|
|
362
|
+
for pattern, description, severity in http_patterns:
|
|
363
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
364
|
+
for match in matches:
|
|
365
|
+
findings.append(
|
|
366
|
+
make_finding(
|
|
367
|
+
severity=severity,
|
|
368
|
+
file=rel_path,
|
|
369
|
+
line_start=i,
|
|
370
|
+
title=description,
|
|
371
|
+
description=f"Plain HTTP URL found in potentially secure context: {match.group(0)}",
|
|
372
|
+
evidence=line.strip(),
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return findings
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Noise Filter — classifies and suppresses findings from non-source files.
|
|
3
|
+
|
|
4
|
+
The scanner's largest false-positive source is not weak detection logic —
|
|
5
|
+
it is scanning files whose contents are *supposed* to look dangerous:
|
|
6
|
+
test fixtures with fake secrets, migration SQL strings, lockfile blobs,
|
|
7
|
+
minified bundles, generated protobuf stubs.
|
|
8
|
+
|
|
9
|
+
NoiseFilter assigns every finding's file path to a category and either
|
|
10
|
+
discards or severity-caps the finding per config (``noise_filter``):
|
|
11
|
+
|
|
12
|
+
enabled default true
|
|
13
|
+
mode "cap" (default: downgrade to info + tag) | "discard"
|
|
14
|
+
skip_tests default true — test_*, *_test.*, conftest, spec/, fixtures/
|
|
15
|
+
skip_locks default true — package-lock.json, yarn.lock, poetry.lock, ...
|
|
16
|
+
skip_generated default true — *.min.js, *_pb2.py, *.map, *_pb2_grpc.py, ...
|
|
17
|
+
skip_docs default true — *.md, *.rst, *.txt, *.csv
|
|
18
|
+
|
|
19
|
+
Honesty rule: capped findings are never deleted silently — they keep their
|
|
20
|
+
data, marked ``noise_category`` so the user can see exactly what was muted
|
|
21
|
+
and why.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import fnmatch
|
|
27
|
+
import logging
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import PurePosixPath
|
|
30
|
+
|
|
31
|
+
_log = logging.getLogger("patchi.noise_filter")
|
|
32
|
+
|
|
33
|
+
# ── Category definitions ─────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
_LOCKFILE_NAMES = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"package-lock.json",
|
|
38
|
+
"npm-shrinkwrap.json",
|
|
39
|
+
"yarn.lock",
|
|
40
|
+
"pnpm-lock.yaml",
|
|
41
|
+
"poetry.lock",
|
|
42
|
+
"Pipfile.lock",
|
|
43
|
+
"Cargo.lock",
|
|
44
|
+
"composer.lock",
|
|
45
|
+
"Gemfile.lock",
|
|
46
|
+
"packages.locker",
|
|
47
|
+
"mix.lock",
|
|
48
|
+
"go.sum",
|
|
49
|
+
"bun.lockb",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_GENERATED_PATTERNS = (
|
|
54
|
+
"*.min.js",
|
|
55
|
+
"*.min.css",
|
|
56
|
+
"*.min.mjs",
|
|
57
|
+
"*.map",
|
|
58
|
+
"*.bundle.js",
|
|
59
|
+
"*_pb2.py",
|
|
60
|
+
"*_pb2_grpc.py",
|
|
61
|
+
"*_pb.py",
|
|
62
|
+
"*.pb.go",
|
|
63
|
+
"*_pb2.pyi",
|
|
64
|
+
"*-gen.py",
|
|
65
|
+
"*_gen.go",
|
|
66
|
+
"*.generated.*",
|
|
67
|
+
"*.g.cs",
|
|
68
|
+
"*.Designer.cs",
|
|
69
|
+
"*.snap",
|
|
70
|
+
"__snapshots__/*",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
_DOC_PATTERNS = ("*.md", "*.mdx", "*.rst", "*.txt", "*.csv", "*.log")
|
|
74
|
+
|
|
75
|
+
_TEST_FILE_PATTERNS = (
|
|
76
|
+
"test_*.py",
|
|
77
|
+
"*_test.py",
|
|
78
|
+
"*_test.go",
|
|
79
|
+
"*_test.dart",
|
|
80
|
+
"conftest.py",
|
|
81
|
+
"test_utils.py",
|
|
82
|
+
"*.spec.ts",
|
|
83
|
+
"*.spec.tsx",
|
|
84
|
+
"*.spec.js",
|
|
85
|
+
"*.spec.jsx",
|
|
86
|
+
"*.spec.mjs",
|
|
87
|
+
"*.test.ts",
|
|
88
|
+
"*.test.tsx",
|
|
89
|
+
"*.test.js",
|
|
90
|
+
"*.test.jsx",
|
|
91
|
+
"*.test.mjs",
|
|
92
|
+
"*_test.rb",
|
|
93
|
+
"*.spec.rb",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
_TEST_DIR_MARKERS = frozenset(
|
|
97
|
+
{
|
|
98
|
+
"test",
|
|
99
|
+
"tests",
|
|
100
|
+
"spec",
|
|
101
|
+
"specs",
|
|
102
|
+
"fixtures",
|
|
103
|
+
"fixture",
|
|
104
|
+
"testdata",
|
|
105
|
+
"test_data",
|
|
106
|
+
"mock",
|
|
107
|
+
"mocks",
|
|
108
|
+
"stubs",
|
|
109
|
+
"__snapshots__",
|
|
110
|
+
"__tests__",
|
|
111
|
+
"testing",
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def classify(path: str) -> str | None:
|
|
117
|
+
"""Return the noise category for a path, or None if it looks like source.
|
|
118
|
+
|
|
119
|
+
Categories: "lockfile", "generated", "docs", "tests".
|
|
120
|
+
Order matters: lockfile > generated > docs > tests.
|
|
121
|
+
"""
|
|
122
|
+
p = PurePosixPath(path.replace("\\", "/"))
|
|
123
|
+
name = p.name
|
|
124
|
+
|
|
125
|
+
# 1. Lockfiles — exact name match anywhere in the tree
|
|
126
|
+
if name in _LOCKFILE_NAMES:
|
|
127
|
+
return "lockfile"
|
|
128
|
+
|
|
129
|
+
# 2. Generated/minified — pattern match on filename
|
|
130
|
+
for pat in _GENERATED_PATTERNS:
|
|
131
|
+
if "/" not in pat and fnmatch.fnmatch(name, pat):
|
|
132
|
+
return "generated"
|
|
133
|
+
|
|
134
|
+
# 3. Docs — extension-based
|
|
135
|
+
for pat in _DOC_PATTERNS:
|
|
136
|
+
if fnmatch.fnmatch(name, pat):
|
|
137
|
+
return "docs"
|
|
138
|
+
|
|
139
|
+
# 4. Tests — filename pattern OR directory marker in path
|
|
140
|
+
for pat in _TEST_FILE_PATTERNS:
|
|
141
|
+
if fnmatch.fnmatch(name, pat):
|
|
142
|
+
return "tests"
|
|
143
|
+
parts = {seg.lower() for seg in p.parts[:-1]}
|
|
144
|
+
if parts & _TEST_DIR_MARKERS:
|
|
145
|
+
return "tests"
|
|
146
|
+
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class NoiseReport:
|
|
152
|
+
"""Summary of what the filter suppressed and why."""
|
|
153
|
+
|
|
154
|
+
total_in: int = 0
|
|
155
|
+
kept: int = 0
|
|
156
|
+
capped: int = 0
|
|
157
|
+
discarded: int = 0
|
|
158
|
+
by_category: dict = field(default_factory=dict)
|
|
159
|
+
|
|
160
|
+
def to_dict(self) -> dict:
|
|
161
|
+
return {
|
|
162
|
+
"total_in": self.total_in,
|
|
163
|
+
"kept": self.kept,
|
|
164
|
+
"capped": self.capped,
|
|
165
|
+
"discarded": self.discarded,
|
|
166
|
+
"by_category": dict(self.by_category),
|
|
167
|
+
"statement": (
|
|
168
|
+
f"{self.kept}/{self.total_in} findings kept "
|
|
169
|
+
f"({self.capped} noise-capped, {self.discarded} discarded)"
|
|
170
|
+
),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class NoiseFilter:
|
|
175
|
+
"""Filters or severity-caps findings originating from noise files."""
|
|
176
|
+
|
|
177
|
+
def __init__(self, root=None, config: dict | None = None):
|
|
178
|
+
cfg = (config or {}).get("noise_filter", {})
|
|
179
|
+
self.enabled: bool = bool(cfg.get("enabled", True))
|
|
180
|
+
self.mode: str = cfg.get("mode", "cap") # "cap" | "discard"
|
|
181
|
+
self.skip_tests: bool = bool(cfg.get("skip_tests", True))
|
|
182
|
+
self.skip_locks: bool = bool(cfg.get("skip_locks", True))
|
|
183
|
+
self.skip_generated: bool = bool(cfg.get("skip_generated", True))
|
|
184
|
+
self.skip_docs: bool = bool(cfg.get("skip_docs", True))
|
|
185
|
+
|
|
186
|
+
# ── Single-path API ─────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
def category_for(self, path: str) -> str | None:
|
|
189
|
+
"""Category for this path, honoring the skip_* toggles."""
|
|
190
|
+
cat = classify(path)
|
|
191
|
+
if cat == "lockfile" and not self.skip_locks:
|
|
192
|
+
return None
|
|
193
|
+
if cat == "generated" and not self.skip_generated:
|
|
194
|
+
return None
|
|
195
|
+
if cat == "docs" and not self.skip_docs:
|
|
196
|
+
return None
|
|
197
|
+
if cat == "tests" and not self.skip_tests:
|
|
198
|
+
return None
|
|
199
|
+
return cat
|
|
200
|
+
|
|
201
|
+
def is_noise(self, path: str) -> bool:
|
|
202
|
+
return self.category_for(path) is not None
|
|
203
|
+
|
|
204
|
+
# ── Finding-list API ────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
def apply(self, findings: list) -> tuple[list, NoiseReport]:
|
|
207
|
+
"""Split findings into (kept, report).
|
|
208
|
+
|
|
209
|
+
Accepts Finding objects *or* plain dicts (as produced by
|
|
210
|
+
``merge_results``); each entry needs a ``file`` key/attribute.
|
|
211
|
+
In "cap" mode noisy findings stay in the kept list but their
|
|
212
|
+
severity is downgraded to info and they gain a ``noise_category``
|
|
213
|
+
annotation. In "discard" mode they are removed entirely.
|
|
214
|
+
"""
|
|
215
|
+
report = NoiseReport(total_in=len(findings))
|
|
216
|
+
kept: list = []
|
|
217
|
+
for f in findings:
|
|
218
|
+
path = _get(f, "file", "") or ""
|
|
219
|
+
cat = self.category_for(path) if (self.enabled and path) else None
|
|
220
|
+
if cat is None:
|
|
221
|
+
kept.append(f)
|
|
222
|
+
report.kept += 1
|
|
223
|
+
continue
|
|
224
|
+
report.by_category[cat] = report.by_category.get(cat, 0) + 1
|
|
225
|
+
if self.mode == "discard":
|
|
226
|
+
report.discarded += 1
|
|
227
|
+
continue
|
|
228
|
+
# cap mode: downgrade + annotate, never delete silently
|
|
229
|
+
_set_severity_info(f)
|
|
230
|
+
_set(f, "noise_category", cat)
|
|
231
|
+
kept.append(f)
|
|
232
|
+
report.capped += 1
|
|
233
|
+
report.kept += 1
|
|
234
|
+
if report.total_in and (report.capped or report.discarded):
|
|
235
|
+
_log.info(
|
|
236
|
+
"Noise filter: %d/%d findings from noise files (%s)",
|
|
237
|
+
report.capped + report.discarded,
|
|
238
|
+
report.total_in,
|
|
239
|
+
", ".join(f"{k}={v}" for k, v in sorted(report.by_category.items())),
|
|
240
|
+
)
|
|
241
|
+
return kept, report
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ── Polymorphic accessors (Finding objects vs merged dicts) ─────────────
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _get(f, key: str, default=None):
|
|
248
|
+
if isinstance(f, dict):
|
|
249
|
+
return f.get(key, default)
|
|
250
|
+
return getattr(f, key, default)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _set(f, key: str, value) -> None:
|
|
254
|
+
if isinstance(f, dict):
|
|
255
|
+
f[key] = value
|
|
256
|
+
return
|
|
257
|
+
try:
|
|
258
|
+
setattr(f, key, value)
|
|
259
|
+
except Exception as _exc: # noqa: BLE001 — frozen/sealed objects: skip silently
|
|
260
|
+
_log.debug('_set skipped: %s', _exc)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _set_severity_info(f) -> None:
|
|
264
|
+
"""Downgrade severity to INFO, matching the container's own encoding."""
|
|
265
|
+
if isinstance(f, dict):
|
|
266
|
+
# merge_results dicts carry severity as a plain string
|
|
267
|
+
f["severity"] = "info"
|
|
268
|
+
return
|
|
269
|
+
try:
|
|
270
|
+
from patchi.core.agents.base import Severity
|
|
271
|
+
|
|
272
|
+
f.severity = Severity.INFO
|
|
273
|
+
except Exception as _exc: # noqa: BLE001 — frozen/sealed objects: annotate only
|
|
274
|
+
_log.debug('_set_severity_info skipped: %s', _exc)
|