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,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App Mapper — crawls a live deployed app and builds a structured map.
|
|
3
|
+
|
|
4
|
+
Uses urllib (no external deps) or Crawl4AI when available.
|
|
5
|
+
Returns structured map of pages, forms, links, and input fields.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.request
|
|
14
|
+
from html.parser import HTMLParser
|
|
15
|
+
from urllib.parse import urljoin, urlparse
|
|
16
|
+
|
|
17
|
+
from patchi.core.agents.base import (
|
|
18
|
+
AgentGroup,
|
|
19
|
+
AgentInput,
|
|
20
|
+
AgentResult,
|
|
21
|
+
BaseAgent,
|
|
22
|
+
Finding,
|
|
23
|
+
Severity,
|
|
24
|
+
register,
|
|
25
|
+
)
|
|
26
|
+
from patchi.core.constants import is_offline
|
|
27
|
+
|
|
28
|
+
_log = logging.getLogger("patchi.security.app_mapper")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _LinkFormParser(HTMLParser):
|
|
32
|
+
"""Extract links and forms from HTML."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, base_url: str):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.base_url = base_url
|
|
37
|
+
self.links: list[str] = []
|
|
38
|
+
self.forms: list[dict] = []
|
|
39
|
+
self._in_form = False
|
|
40
|
+
self._form_action = ""
|
|
41
|
+
self._form_method = "GET"
|
|
42
|
+
self._form_fields: list[dict] = []
|
|
43
|
+
self._title = ""
|
|
44
|
+
|
|
45
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]):
|
|
46
|
+
attr_dict = {k: v or "" for k, v in attrs}
|
|
47
|
+
|
|
48
|
+
if tag == "title":
|
|
49
|
+
pass # handled in data
|
|
50
|
+
|
|
51
|
+
if tag == "a":
|
|
52
|
+
href = attr_dict.get("href", "")
|
|
53
|
+
if href and not href.startswith(("#", "javascript:", "mailto:")):
|
|
54
|
+
full = urljoin(self.base_url, href)
|
|
55
|
+
self.links.append(full)
|
|
56
|
+
|
|
57
|
+
if tag == "form":
|
|
58
|
+
self._in_form = True
|
|
59
|
+
self._form_action = urljoin(self.base_url, attr_dict.get("action", self.base_url))
|
|
60
|
+
self._form_method = attr_dict.get("method", "GET").upper()
|
|
61
|
+
self._form_fields = []
|
|
62
|
+
|
|
63
|
+
if tag == "input" and self._in_form:
|
|
64
|
+
self._form_fields.append(
|
|
65
|
+
{
|
|
66
|
+
"name": attr_dict.get("name", ""),
|
|
67
|
+
"type": attr_dict.get("type", "text"),
|
|
68
|
+
"placeholder": attr_dict.get("placeholder", ""),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if tag == "select" and self._in_form:
|
|
73
|
+
self._form_fields.append(
|
|
74
|
+
{
|
|
75
|
+
"name": attr_dict.get("name", ""),
|
|
76
|
+
"type": "select",
|
|
77
|
+
"placeholder": "",
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if tag == "textarea" and self._in_form:
|
|
82
|
+
self._form_fields.append(
|
|
83
|
+
{
|
|
84
|
+
"name": attr_dict.get("name", ""),
|
|
85
|
+
"type": "textarea",
|
|
86
|
+
"placeholder": "",
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def handle_endtag(self, tag: str):
|
|
91
|
+
if tag == "form" and self._in_form:
|
|
92
|
+
self._in_form = False
|
|
93
|
+
self.forms.append(
|
|
94
|
+
{
|
|
95
|
+
"action": self._form_action,
|
|
96
|
+
"method": self._form_method,
|
|
97
|
+
"fields": self._form_fields,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def handle_data(self, data: str):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def patchi_map_app(url: str, max_pages: int = 20) -> dict:
|
|
106
|
+
"""Crawl a live app and return structured map."""
|
|
107
|
+
if is_offline():
|
|
108
|
+
return {"pages": [], "total_pages": 0, "total_entry_points": 0}
|
|
109
|
+
visited: set[str] = set()
|
|
110
|
+
pages: list[dict] = []
|
|
111
|
+
queue = [url]
|
|
112
|
+
|
|
113
|
+
while queue and len(pages) < max_pages:
|
|
114
|
+
current = queue.pop(0)
|
|
115
|
+
if current in visited:
|
|
116
|
+
continue
|
|
117
|
+
visited.add(current)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
req = urllib.request.Request(current, headers={"User-Agent": "Patchi-AppMapper/1.0"})
|
|
121
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
122
|
+
content_type = resp.headers.get("Content-Type", "")
|
|
123
|
+
if "text/html" not in content_type:
|
|
124
|
+
continue
|
|
125
|
+
html = resp.read().decode("utf-8", errors="ignore")
|
|
126
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError):
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
parser = _LinkFormParser(current)
|
|
130
|
+
try:
|
|
131
|
+
parser.feed(html)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
_log.warning("patchi_map_app failed: %s", e)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Extract title
|
|
137
|
+
title_match = re.search(r"<title[^>]*>(.*?)</title>", html, re.I | re.DOTALL)
|
|
138
|
+
title = title_match.group(1).strip() if title_match else ""
|
|
139
|
+
|
|
140
|
+
pages.append(
|
|
141
|
+
{
|
|
142
|
+
"url": current,
|
|
143
|
+
"title": title,
|
|
144
|
+
"forms": parser.forms,
|
|
145
|
+
"links": list(set(parser.links)),
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Queue new links (same origin only)
|
|
150
|
+
origin = urlparse(url).netloc
|
|
151
|
+
for link in parser.links:
|
|
152
|
+
if urlparse(link).netloc == origin and link not in visited:
|
|
153
|
+
queue.append(link)
|
|
154
|
+
|
|
155
|
+
total_entry_points = sum(len(p["forms"]) + len(p["links"]) for p in pages)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"pages": pages,
|
|
159
|
+
"total_pages": len(pages),
|
|
160
|
+
"total_entry_points": total_entry_points,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ── App Mapper Agent ──────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@register
|
|
168
|
+
class AppMapperAgent(BaseAgent):
|
|
169
|
+
"""Crawls live app and maps pages, forms, and entry points."""
|
|
170
|
+
|
|
171
|
+
name = "AppMapperAgent"
|
|
172
|
+
group = AgentGroup.SECURITY
|
|
173
|
+
timeout = 60
|
|
174
|
+
|
|
175
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
176
|
+
app_url = (
|
|
177
|
+
inp.brain.get("app_url", "")
|
|
178
|
+
or inp.config.get("app_url", "")
|
|
179
|
+
or inp.extra.get("app_url", "")
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if not app_url:
|
|
183
|
+
result.add_finding(
|
|
184
|
+
Finding(
|
|
185
|
+
agent=self.name,
|
|
186
|
+
type="no_target_url",
|
|
187
|
+
severity=Severity.INFO,
|
|
188
|
+
file="",
|
|
189
|
+
message="No app URL configured — set 'app_url' in config to enable app mapping",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
app_map = patchi_map_app(app_url)
|
|
195
|
+
result.data["app_map"] = app_map
|
|
196
|
+
result.data["total_pages"] = app_map["total_pages"]
|
|
197
|
+
result.data["total_entry_points"] = app_map["total_entry_points"]
|
|
198
|
+
|
|
199
|
+
# Flag forms without CSRF protection indicators
|
|
200
|
+
for page in app_map["pages"]:
|
|
201
|
+
for form in page["forms"]:
|
|
202
|
+
if form["method"] == "POST":
|
|
203
|
+
field_names = [f["name"].lower() for f in form["fields"]]
|
|
204
|
+
if not any("csrf" in n or "token" in n or "_token" in n for n in field_names):
|
|
205
|
+
result.add_finding(
|
|
206
|
+
Finding(
|
|
207
|
+
agent=self.name,
|
|
208
|
+
type="no_csrf_token",
|
|
209
|
+
severity=Severity.MEDIUM,
|
|
210
|
+
file=page["url"],
|
|
211
|
+
message=f"POST form at {form['action']} has no CSRF token field",
|
|
212
|
+
cwe="CWE-352",
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
result.files_scanned = app_map["total_pages"]
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App Profile — component-level project profiling with domain scoring.
|
|
3
|
+
|
|
4
|
+
Detects what kind of project this is (CLI binary, backend API, frontend web,
|
|
5
|
+
library, mobile, etc.) and activates/deactivates security domains accordingly.
|
|
6
|
+
|
|
7
|
+
Three activation states per domain:
|
|
8
|
+
- Active: Domain applies to this project type
|
|
9
|
+
- N/A: Domain does not apply (e.g. web-frontend for a CLI tool)
|
|
10
|
+
- Unclear: Cannot determine — falls to heuristic/AI
|
|
11
|
+
|
|
12
|
+
Scoring:
|
|
13
|
+
- Only Active domains contribute to the weighted score
|
|
14
|
+
- context_score = Σ(domain_weight × domain_score) / Σ(domain_weight)
|
|
15
|
+
- N/A domains are excluded from both numerator and denominator
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from patchi.core.security.domain_loader import Domain, DomainLoader, normalize_component_type
|
|
26
|
+
|
|
27
|
+
_log = logging.getLogger("patchi.security.app_profile")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ActivationState(str):
|
|
31
|
+
ACTIVE = "active"
|
|
32
|
+
NA = "n/a"
|
|
33
|
+
UNCLEAR = "unclear"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class DomainActivation:
|
|
38
|
+
domain_id: str
|
|
39
|
+
display_name: str
|
|
40
|
+
state: str # active | n/a | unclear
|
|
41
|
+
weight: float
|
|
42
|
+
score: float = 0.0
|
|
43
|
+
reason: str = ""
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
return {
|
|
47
|
+
"domain_id": self.domain_id,
|
|
48
|
+
"display_name": self.display_name,
|
|
49
|
+
"state": self.state,
|
|
50
|
+
"weight": self.weight,
|
|
51
|
+
"score": round(self.score, 1),
|
|
52
|
+
"reason": self.reason,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ComponentProfile:
|
|
58
|
+
component_type: (
|
|
59
|
+
str # cli-binary | backend-api | frontend-web | library | mobile | embedded | unknown
|
|
60
|
+
)
|
|
61
|
+
display_name: str = ""
|
|
62
|
+
frameworks: list[str] = field(default_factory=list)
|
|
63
|
+
languages: list[str] = field(default_factory=list)
|
|
64
|
+
has_database: bool = False
|
|
65
|
+
has_authentication: bool = False
|
|
66
|
+
has_network: bool = False
|
|
67
|
+
has_user_input: bool = False
|
|
68
|
+
is_packaged: bool = False
|
|
69
|
+
detection_signals: dict[str, Any] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict:
|
|
72
|
+
return {
|
|
73
|
+
"component_type": self.component_type,
|
|
74
|
+
"display_name": self.display_name,
|
|
75
|
+
"frameworks": self.frameworks,
|
|
76
|
+
"languages": self.languages,
|
|
77
|
+
"has_database": self.has_database,
|
|
78
|
+
"has_authentication": self.has_authentication,
|
|
79
|
+
"has_network": self.has_network,
|
|
80
|
+
"has_user_input": self.has_user_input,
|
|
81
|
+
"is_packaged": self.is_packaged,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_COMPONENT_SIGNALS: dict[str, dict] = {
|
|
86
|
+
"backend-api": {
|
|
87
|
+
"frameworks": {
|
|
88
|
+
"fastapi",
|
|
89
|
+
"flask",
|
|
90
|
+
"django",
|
|
91
|
+
"express",
|
|
92
|
+
"koa",
|
|
93
|
+
"spring",
|
|
94
|
+
"actix",
|
|
95
|
+
"rocket",
|
|
96
|
+
"gin",
|
|
97
|
+
"echo",
|
|
98
|
+
},
|
|
99
|
+
"files": {"routes/", "api/", "controllers/", "middleware/", "serializers/"},
|
|
100
|
+
"deps": {"fastapi", "flask", "django", "express", "axios", "rest"},
|
|
101
|
+
},
|
|
102
|
+
"frontend-web": {
|
|
103
|
+
"frameworks": {"react", "vue", "angular", "svelte", "next", "nuxt", "solid"},
|
|
104
|
+
"files": {"components/", "pages/", "views/", "templates/"},
|
|
105
|
+
"deps": {"react", "vue", "angular", "svelte"},
|
|
106
|
+
},
|
|
107
|
+
"cli-binary": {
|
|
108
|
+
"frameworks": {"click", "typer", "argparse", "clap", "cobra", "commander"},
|
|
109
|
+
"files": {"cli/", "commands/"},
|
|
110
|
+
"deps": {"click", "typer", "argparse", "clap"},
|
|
111
|
+
},
|
|
112
|
+
"library": {
|
|
113
|
+
"frameworks": set(),
|
|
114
|
+
"files": {"src/", "lib/"},
|
|
115
|
+
"deps": set(),
|
|
116
|
+
},
|
|
117
|
+
"mobile": {
|
|
118
|
+
"frameworks": {"react-native", "flutter", "swiftui", "jetpack"},
|
|
119
|
+
"files": {"android/", "ios/", "lib/"},
|
|
120
|
+
"deps": {"react-native", "flutter"},
|
|
121
|
+
},
|
|
122
|
+
"embedded": {
|
|
123
|
+
"frameworks": set(),
|
|
124
|
+
"files": {"firmware/", "hal/", "drivers/"},
|
|
125
|
+
"deps": set(),
|
|
126
|
+
},
|
|
127
|
+
"infra": {
|
|
128
|
+
"frameworks": {
|
|
129
|
+
"docker",
|
|
130
|
+
"kubernetes",
|
|
131
|
+
"terraform",
|
|
132
|
+
"ansible",
|
|
133
|
+
"pulumi",
|
|
134
|
+
"helm",
|
|
135
|
+
"kustomize",
|
|
136
|
+
},
|
|
137
|
+
"files": {
|
|
138
|
+
"Dockerfile",
|
|
139
|
+
"docker-compose",
|
|
140
|
+
".github/workflows/",
|
|
141
|
+
".gitlab-ci/",
|
|
142
|
+
"k8s/",
|
|
143
|
+
"deploy/",
|
|
144
|
+
"infra/",
|
|
145
|
+
},
|
|
146
|
+
"deps": {"docker", "docker-compose", "kubernetes", "terraform"},
|
|
147
|
+
},
|
|
148
|
+
"llm-integration": {
|
|
149
|
+
"frameworks": {"langchain", "llamaindex", "openai", "anthropic", "cohere", "transformers"},
|
|
150
|
+
"files": {"llm/", "ai/", "prompts/", "embeddings/", "vector-store/", "agents/"},
|
|
151
|
+
"deps": {"openai", "langchain", "anthropic", "cohere", "transformers"},
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_COMPONENT_DOMAIN_MAP: dict[str, list[str]] = {
|
|
156
|
+
"backend-api": ["backend-api", "web-frontend"],
|
|
157
|
+
"frontend-web": ["web-frontend"],
|
|
158
|
+
"frontend": ["web-frontend"],
|
|
159
|
+
"cli-binary": [],
|
|
160
|
+
"library": [],
|
|
161
|
+
"mobile": ["web-frontend"],
|
|
162
|
+
"infra": ["cicd-pipeline", "container-infra", "supply-chain-local-tool"],
|
|
163
|
+
"infrastructure": ["cicd-pipeline", "container-infra", "supply-chain-local-tool"],
|
|
164
|
+
"llm-integration": ["llm-integration"],
|
|
165
|
+
"embedded": [],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class AppProfileBuilder:
|
|
170
|
+
"""Builds an AppProfile from project files and brain data."""
|
|
171
|
+
|
|
172
|
+
def __init__(self, root: Path):
|
|
173
|
+
self.root = root
|
|
174
|
+
|
|
175
|
+
def build(self, brain: dict | None = None) -> ComponentProfile:
|
|
176
|
+
"""Detect component type from project files and brain data."""
|
|
177
|
+
profile = ComponentProfile(component_type="unknown")
|
|
178
|
+
brain = brain or {}
|
|
179
|
+
|
|
180
|
+
# Languages from brain
|
|
181
|
+
lang_data = brain.get("languages", {})
|
|
182
|
+
if isinstance(lang_data, dict):
|
|
183
|
+
profile.languages = list(lang_data.keys())
|
|
184
|
+
elif isinstance(lang_data, list):
|
|
185
|
+
profile.languages = [str(lang) for lang in lang_data]
|
|
186
|
+
|
|
187
|
+
# Frameworks from brain
|
|
188
|
+
fw_raw = brain.get("frameworks", brain.get("framework", ""))
|
|
189
|
+
if isinstance(fw_raw, list):
|
|
190
|
+
profile.frameworks = [
|
|
191
|
+
f if isinstance(f, str) else (f.get("name", "") or "") for f in fw_raw
|
|
192
|
+
]
|
|
193
|
+
elif isinstance(fw_raw, dict):
|
|
194
|
+
profile.frameworks = [fw_raw.get("name", "")] if fw_raw.get("name") else []
|
|
195
|
+
elif isinstance(fw_raw, str):
|
|
196
|
+
profile.frameworks = [fw_raw] if fw_raw else []
|
|
197
|
+
|
|
198
|
+
profile.frameworks = [f.lower() for f in profile.frameworks if f]
|
|
199
|
+
|
|
200
|
+
# Detection signals from file system
|
|
201
|
+
profile.detection_signals = self._scan_project_files()
|
|
202
|
+
|
|
203
|
+
# Component type inference
|
|
204
|
+
profile.component_type = self._infer_component_type(profile)
|
|
205
|
+
profile.display_name = (
|
|
206
|
+
brain.get("project_purpose", brain.get("project_name", "")) or profile.component_type
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
profile.has_database = profile.detection_signals.get("has_database", False)
|
|
210
|
+
profile.has_authentication = profile.detection_signals.get("has_authentication", False)
|
|
211
|
+
profile.has_network = profile.detection_signals.get("has_network", False)
|
|
212
|
+
profile.has_user_input = profile.detection_signals.get("has_user_input", False)
|
|
213
|
+
profile.is_packaged = profile.detection_signals.get("is_packaged", False)
|
|
214
|
+
|
|
215
|
+
return profile
|
|
216
|
+
|
|
217
|
+
def _scan_project_files(self) -> dict[str, Any]:
|
|
218
|
+
"""Scan root for file-level signals (fast, no full AST)."""
|
|
219
|
+
signals: dict[str, Any] = {
|
|
220
|
+
"has_database": False,
|
|
221
|
+
"has_authentication": False,
|
|
222
|
+
"has_network": False,
|
|
223
|
+
"has_user_input": False,
|
|
224
|
+
"is_packaged": False,
|
|
225
|
+
"found_dirs": set(),
|
|
226
|
+
"found_deps": set(),
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
pyproject = self.root / "pyproject.toml"
|
|
230
|
+
if pyproject.exists():
|
|
231
|
+
signals["is_packaged"] = True
|
|
232
|
+
try:
|
|
233
|
+
content = pyproject.read_text(encoding="utf-8", errors="ignore")
|
|
234
|
+
if "django" in content:
|
|
235
|
+
signals["found_deps"].add("django")
|
|
236
|
+
if "fastapi" in content:
|
|
237
|
+
signals["found_deps"].add("fastapi")
|
|
238
|
+
if "click" in content or "typer" in content:
|
|
239
|
+
signals["found_deps"].add("click")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
_log.warning("AppProfileBuilder._scan_project_files failed: %s", e)
|
|
242
|
+
|
|
243
|
+
package = self.root / "package.json"
|
|
244
|
+
if package.exists():
|
|
245
|
+
signals["is_packaged"] = True
|
|
246
|
+
try:
|
|
247
|
+
import json
|
|
248
|
+
|
|
249
|
+
data = json.loads(package.read_text(encoding="utf-8", errors="ignore"))
|
|
250
|
+
deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
|
|
251
|
+
signals["found_deps"].update(deps.keys())
|
|
252
|
+
except Exception as e:
|
|
253
|
+
_log.warning("AppProfileBuilder._scan_project_files failed: %s", e)
|
|
254
|
+
|
|
255
|
+
# Directory structure scan (limited depth)
|
|
256
|
+
for entry in self.root.iterdir():
|
|
257
|
+
if entry.is_dir() and not entry.name.startswith("."):
|
|
258
|
+
name = entry.name.lower()
|
|
259
|
+
if name in ("routes", "api", "controllers", "middleware"):
|
|
260
|
+
signals["found_dirs"].add(name)
|
|
261
|
+
signals["has_network"] = True
|
|
262
|
+
if name in ("components", "pages", "views", "templates"):
|
|
263
|
+
signals["found_dirs"].add(name)
|
|
264
|
+
signals["has_user_input"] = True
|
|
265
|
+
if name in ("cli", "commands"):
|
|
266
|
+
signals["found_dirs"].add(name)
|
|
267
|
+
if name in ("models", "migrations", "db", "database"):
|
|
268
|
+
signals["found_dirs"].add(name)
|
|
269
|
+
signals["has_database"] = True
|
|
270
|
+
if name in ("auth", "login", "sessions"):
|
|
271
|
+
signals["found_dirs"].add(name)
|
|
272
|
+
signals["has_authentication"] = True
|
|
273
|
+
|
|
274
|
+
return signals
|
|
275
|
+
|
|
276
|
+
def _infer_component_type(self, profile: ComponentProfile) -> str:
|
|
277
|
+
"""Score each component type and return the best match."""
|
|
278
|
+
scores: dict[str, int] = dict.fromkeys(_COMPONENT_SIGNALS, 0)
|
|
279
|
+
signals = profile.detection_signals
|
|
280
|
+
|
|
281
|
+
for ct, sig in _COMPONENT_SIGNALS.items():
|
|
282
|
+
# Framework matches
|
|
283
|
+
for fw in profile.frameworks:
|
|
284
|
+
if fw in sig.get("frameworks", set()):
|
|
285
|
+
scores[ct] += 3
|
|
286
|
+
|
|
287
|
+
# Directory matches
|
|
288
|
+
for d in signals.get("found_dirs", set()):
|
|
289
|
+
if d + "/" in sig.get("files", set()):
|
|
290
|
+
scores[ct] += 2
|
|
291
|
+
|
|
292
|
+
# Dependency matches
|
|
293
|
+
for dep in signals.get("found_deps", set()):
|
|
294
|
+
if any(k in dep for k in sig.get("deps", set())):
|
|
295
|
+
scores[ct] += 1
|
|
296
|
+
|
|
297
|
+
best = max(scores, key=scores.get)
|
|
298
|
+
return best if scores[best] > 0 else "unknown"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class AppProfileScorer:
|
|
302
|
+
"""Computes weighted domain scores from an AppProfile."""
|
|
303
|
+
|
|
304
|
+
def __init__(self, root: Path):
|
|
305
|
+
self.root = root
|
|
306
|
+
self._loader = DomainLoader(root)
|
|
307
|
+
|
|
308
|
+
def score(
|
|
309
|
+
self, profile: ComponentProfile, existing_scores: dict[str, float] | None = None
|
|
310
|
+
) -> list[DomainActivation]:
|
|
311
|
+
"""Score all domains against the component profile."""
|
|
312
|
+
existing_scores = existing_scores or {}
|
|
313
|
+
domain_ids = self._get_applicable_domains(profile)
|
|
314
|
+
results: list[DomainActivation] = []
|
|
315
|
+
|
|
316
|
+
for did in domain_ids:
|
|
317
|
+
domain = self._loader.get_domain(did)
|
|
318
|
+
if not domain:
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
state, reason = self._determine_activation(domain, profile)
|
|
322
|
+
score = existing_scores.get(did, 50.0)
|
|
323
|
+
|
|
324
|
+
results.append(
|
|
325
|
+
DomainActivation(
|
|
326
|
+
domain_id=did,
|
|
327
|
+
display_name=domain.display_name,
|
|
328
|
+
state=state,
|
|
329
|
+
weight=domain.weight,
|
|
330
|
+
score=score if state == ActivationState.ACTIVE else 0.0,
|
|
331
|
+
reason=reason,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return results
|
|
336
|
+
|
|
337
|
+
def compute_context_score(self, activations: list[DomainActivation]) -> float:
|
|
338
|
+
"""Weighted sum of active domain scores."""
|
|
339
|
+
total_weight = 0.0
|
|
340
|
+
weighted_sum = 0.0
|
|
341
|
+
for a in activations:
|
|
342
|
+
if a.state == ActivationState.ACTIVE:
|
|
343
|
+
total_weight += a.weight
|
|
344
|
+
weighted_sum += a.weight * a.score
|
|
345
|
+
|
|
346
|
+
if total_weight == 0:
|
|
347
|
+
return 50.0
|
|
348
|
+
return weighted_sum / total_weight
|
|
349
|
+
|
|
350
|
+
_TYPE_ALIASES: dict[str, str] = {
|
|
351
|
+
"infrastructure": "infra",
|
|
352
|
+
"frontend": "frontend-web",
|
|
353
|
+
"web-frontend": "frontend-web",
|
|
354
|
+
"backend-api,frontend-web": "backend-api",
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
def _normalize_type(self, ct: str) -> str:
|
|
358
|
+
# Delegate to the loader's canonical normalization (same alias table)
|
|
359
|
+
# so scoped loading and applicability checks can never disagree.
|
|
360
|
+
ct = (ct or "").strip().lower()
|
|
361
|
+
if ct in self._TYPE_ALIASES:
|
|
362
|
+
return self._TYPE_ALIASES[ct]
|
|
363
|
+
return normalize_component_type(ct)
|
|
364
|
+
|
|
365
|
+
def _get_applicable_domains(self, profile: ComponentProfile) -> list[str]:
|
|
366
|
+
"""Return domain IDs that may apply to this component type.
|
|
367
|
+
|
|
368
|
+
The loader is scoped to the profile's component type before enumerating,
|
|
369
|
+
so a narrow profile only ever parses the domain files that could match
|
|
370
|
+
(plus the explicit component->domain map entries) instead of the full
|
|
371
|
+
taxonomy.
|
|
372
|
+
"""
|
|
373
|
+
profile_type = self._normalize_type(profile.component_type)
|
|
374
|
+
explicit = list(_COMPONENT_DOMAIN_MAP.get(profile.component_type, []))
|
|
375
|
+
|
|
376
|
+
# Both axes: component-type match AND explicit domain ids (e.g. a
|
|
377
|
+
# backend-api profile always loads the web-frontend domain).
|
|
378
|
+
self._loader.set_component_scope(
|
|
379
|
+
component_types=[profile.component_type], domain_ids=explicit
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
all_domains = self._loader.list_domains()
|
|
383
|
+
for d in all_domains:
|
|
384
|
+
if d not in explicit:
|
|
385
|
+
domain = self._loader.get_domain(d)
|
|
386
|
+
if domain and domain.component_type:
|
|
387
|
+
comp_types = [
|
|
388
|
+
self._normalize_type(ct.strip()) for ct in domain.component_type.split(",")
|
|
389
|
+
]
|
|
390
|
+
if profile_type in comp_types:
|
|
391
|
+
explicit.append(d)
|
|
392
|
+
|
|
393
|
+
return explicit
|
|
394
|
+
|
|
395
|
+
def _determine_activation(self, domain: Domain, profile: ComponentProfile) -> tuple[str, str]:
|
|
396
|
+
"""Determine if a domain is Active, N/A, or Unclear."""
|
|
397
|
+
sig = domain.activation_signals or {}
|
|
398
|
+
|
|
399
|
+
# Check exclusion signals first
|
|
400
|
+
excluded_if = sig.get("excluded_if", [])
|
|
401
|
+
for cond in excluded_if:
|
|
402
|
+
if self._check_signal(cond, profile):
|
|
403
|
+
return ActivationState.NA, f"Excluded by: {cond}"
|
|
404
|
+
|
|
405
|
+
# Check required signals
|
|
406
|
+
required_any = sig.get("required_any", [])
|
|
407
|
+
if required_any:
|
|
408
|
+
for cond in required_any:
|
|
409
|
+
if self._check_signal(cond, profile):
|
|
410
|
+
return ActivationState.ACTIVE, f"Matched: {cond}"
|
|
411
|
+
return ActivationState.UNCLEAR, f"No required signal matched: {required_any}"
|
|
412
|
+
|
|
413
|
+
# Check framework match
|
|
414
|
+
fw = sig.get("framework", "")
|
|
415
|
+
if fw and fw.lower() in [f.lower() for f in profile.frameworks]:
|
|
416
|
+
return ActivationState.ACTIVE, f"Framework matched: {fw}"
|
|
417
|
+
|
|
418
|
+
# Default: active if component type is relevant (handles comma-separated composite types)
|
|
419
|
+
comp_type = sig.get("component_type", "")
|
|
420
|
+
if comp_type and profile.component_type in [ct.strip() for ct in comp_type.split(",")]:
|
|
421
|
+
return ActivationState.ACTIVE, f"Component type: {comp_type}"
|
|
422
|
+
|
|
423
|
+
return ActivationState.UNCLEAR, "No signals matched — unable to determine activation"
|
|
424
|
+
|
|
425
|
+
def _check_signal(self, condition: str, profile: ComponentProfile) -> bool:
|
|
426
|
+
"""Check a single activation condition string."""
|
|
427
|
+
cond = condition.lower()
|
|
428
|
+
|
|
429
|
+
# Framework checks — check if any framework name appears as a keyword in the signal
|
|
430
|
+
for fw in profile.frameworks:
|
|
431
|
+
fw_key = fw.lower().replace("-", "").replace("_", "")
|
|
432
|
+
cond_key = cond.replace("-", "").replace("_", "").replace("(", "").replace(")", "")
|
|
433
|
+
if fw_key in cond_key:
|
|
434
|
+
return True
|
|
435
|
+
|
|
436
|
+
# Dependency checks — check known dependency keywords
|
|
437
|
+
for dep in profile.detection_signals.get("found_deps", set()):
|
|
438
|
+
dep_key = dep.lower().replace("-", "").replace("_", "")
|
|
439
|
+
if dep_key in cond:
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
# Component type check
|
|
443
|
+
if profile.component_type in cond:
|
|
444
|
+
return True
|
|
445
|
+
|
|
446
|
+
# Directory structure checks
|
|
447
|
+
for d in profile.detection_signals.get("found_dirs", set()):
|
|
448
|
+
if d in cond:
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
# Feature checks
|
|
452
|
+
feature_map = {
|
|
453
|
+
"database": profile.has_database,
|
|
454
|
+
"auth": profile.has_authentication,
|
|
455
|
+
"authentication": profile.has_authentication,
|
|
456
|
+
"network": profile.has_network,
|
|
457
|
+
"user_input": profile.has_user_input,
|
|
458
|
+
"packaged": profile.is_packaged,
|
|
459
|
+
}
|
|
460
|
+
for feature, present in feature_map.items():
|
|
461
|
+
if feature in cond and present:
|
|
462
|
+
return True
|
|
463
|
+
|
|
464
|
+
return False
|