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,453 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JWTSecurityAgent — JWT implementation security analysis.
|
|
3
|
+
|
|
4
|
+
Scans for JWT-related security issues:
|
|
5
|
+
- Weak algorithms (HS256 when RS256 should be used, 'none' algorithm)
|
|
6
|
+
- Missing expiration claims (exp, iat)
|
|
7
|
+
- Insecure secret/key storage (hardcoded, weak secrets)
|
|
8
|
+
- Missing audience/issuer validation
|
|
9
|
+
- Token stored in localStorage vs httpOnly cookies
|
|
10
|
+
- Insecure JWT transmission (over non-HTTPS)
|
|
11
|
+
- Key material in version control
|
|
12
|
+
|
|
13
|
+
Uses AST via ast_utils for JWT library call/import detection;
|
|
14
|
+
regex for literal patterns (algorithms, secrets, storage).
|
|
15
|
+
Does NOT call AI. Does NOT write to disk. Does NOT touch the queue.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
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
|
+
)
|
|
36
|
+
from ..brain.ast_utils import find_assignments, find_calls, find_imports
|
|
37
|
+
from ..brain.code_query import (
|
|
38
|
+
js_calls,
|
|
39
|
+
js_property_names,
|
|
40
|
+
lang_for_file,
|
|
41
|
+
parse_js,
|
|
42
|
+
string_literals,
|
|
43
|
+
)
|
|
44
|
+
from ..brain.languages import DEFAULT_IGNORE_DIRS, Lang, detect_language
|
|
45
|
+
|
|
46
|
+
# ── JWT library names used across languages ──────────────────────────────────
|
|
47
|
+
JWT_LIBRARIES: set[str] = {
|
|
48
|
+
"jsonwebtoken",
|
|
49
|
+
"pyjwt",
|
|
50
|
+
"PyJWT",
|
|
51
|
+
"jose",
|
|
52
|
+
"jwt",
|
|
53
|
+
"JJWT",
|
|
54
|
+
"firebase/php-jwt",
|
|
55
|
+
"ruby-jwt",
|
|
56
|
+
"go-jose",
|
|
57
|
+
"jwt-go",
|
|
58
|
+
}
|
|
59
|
+
JWT_API_CALLS: set[str] = {
|
|
60
|
+
"jwt.encode",
|
|
61
|
+
"jwt.decode",
|
|
62
|
+
"jwt.sign",
|
|
63
|
+
"jwt.verify",
|
|
64
|
+
"jwt.create",
|
|
65
|
+
"jwt.parse",
|
|
66
|
+
"JWT.encode",
|
|
67
|
+
"JWT.decode",
|
|
68
|
+
"JWT.verify",
|
|
69
|
+
"encode",
|
|
70
|
+
"decode",
|
|
71
|
+
"sign",
|
|
72
|
+
"verify",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_log = logging.getLogger("patchi.security.jwt_agent")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@register
|
|
80
|
+
class JWTSecurityAgent(BaseAgent):
|
|
81
|
+
"""Agent for detecting JWT implementation vulnerabilities."""
|
|
82
|
+
|
|
83
|
+
group = AgentGroup.SECURITY
|
|
84
|
+
domain = AgentDomain.SECURITY
|
|
85
|
+
name = "JWTSecurityAgent"
|
|
86
|
+
description = (
|
|
87
|
+
"JWT security: weak algorithms, missing expiry, insecure storage, hardcoded secrets"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# ── Literal value markers (matched against parsed string/assignment
|
|
91
|
+
# nodes — never raw text, so comments can't trigger findings) ──────────
|
|
92
|
+
_SECRET_NAMES = ("secret", "jwt_secret", "signing_key", "private_key")
|
|
93
|
+
_KEY_NAMES = ("secret", "key")
|
|
94
|
+
_STORAGE_PREFIXES = ("jwt", "token", "auth", "access")
|
|
95
|
+
_EXPIRY_NAMES = (
|
|
96
|
+
"exp", "expires", "expiry", "expiration", "expires_in", "expiresin", "ttl",
|
|
97
|
+
)
|
|
98
|
+
_ALGO_SEVERITY = {
|
|
99
|
+
"none": (
|
|
100
|
+
Severity.CRITICAL,
|
|
101
|
+
"none algorithm allowed — can bypass signature verification",
|
|
102
|
+
),
|
|
103
|
+
"hs256": (
|
|
104
|
+
Severity.MEDIUM,
|
|
105
|
+
"HS256 symmetric algorithm — consider RS256/ES256 for distributed systems",
|
|
106
|
+
),
|
|
107
|
+
"hs384": (
|
|
108
|
+
Severity.LOW,
|
|
109
|
+
"HS384 symmetric algorithm — acceptable but verify key strength",
|
|
110
|
+
),
|
|
111
|
+
"hs512": (
|
|
112
|
+
Severity.LOW,
|
|
113
|
+
"HS512 symmetric algorithm — acceptable but verify key strength",
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_SOURCE_PATTERNS = [
|
|
118
|
+
"*.py",
|
|
119
|
+
"*.js",
|
|
120
|
+
"*.mjs",
|
|
121
|
+
"*.cjs",
|
|
122
|
+
"*.jsx",
|
|
123
|
+
"*.ts",
|
|
124
|
+
"*.tsx",
|
|
125
|
+
"*.java",
|
|
126
|
+
"*.php",
|
|
127
|
+
"*.rb",
|
|
128
|
+
"*.go",
|
|
129
|
+
"*.rs",
|
|
130
|
+
"*.kt",
|
|
131
|
+
"*.swift",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
_SKIP_DIRS = DEFAULT_IGNORE_DIRS | {"test", "tests", "vendor"}
|
|
135
|
+
|
|
136
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
137
|
+
findings = []
|
|
138
|
+
files_scanned = 0
|
|
139
|
+
|
|
140
|
+
for pattern in self._SOURCE_PATTERNS:
|
|
141
|
+
for p in self._safe_rglob(inp.root, pattern):
|
|
142
|
+
rel = str(p.relative_to(inp.root))
|
|
143
|
+
if self._should_skip(rel):
|
|
144
|
+
continue
|
|
145
|
+
files_scanned += 1
|
|
146
|
+
try:
|
|
147
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
_log.warning("JWTSecurityAgent._run failed: %s", e)
|
|
150
|
+
continue
|
|
151
|
+
lang = detect_language(p)
|
|
152
|
+
findings.extend(self._analyze_file(content, lang, rel))
|
|
153
|
+
findings.extend(self._check_no_jwt_in_codebase(content, rel))
|
|
154
|
+
|
|
155
|
+
result.status = AgentStatus.DONE
|
|
156
|
+
result.findings = findings
|
|
157
|
+
result.files_scanned = files_scanned
|
|
158
|
+
result.data.update({"files_scanned": files_scanned, "finding_count": len(findings)})
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _unquote(value: str) -> str:
|
|
163
|
+
value = value.strip()
|
|
164
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'`":
|
|
165
|
+
return value[1:-1]
|
|
166
|
+
return value
|
|
167
|
+
|
|
168
|
+
def _analyze_file(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
|
|
169
|
+
findings = []
|
|
170
|
+
lines = content.splitlines()
|
|
171
|
+
|
|
172
|
+
def _snippet(line_num: int) -> str:
|
|
173
|
+
if 0 < line_num <= len(lines):
|
|
174
|
+
return lines[line_num - 1].strip()
|
|
175
|
+
return ""
|
|
176
|
+
|
|
177
|
+
# Detect JWT usage via AST (calls + imports)
|
|
178
|
+
jwt_calls = find_calls(content, lang, JWT_API_CALLS)
|
|
179
|
+
jwt_imports = find_imports(content, lang, JWT_LIBRARIES)
|
|
180
|
+
has_jwt = bool(jwt_calls) or bool(jwt_imports)
|
|
181
|
+
|
|
182
|
+
# String literals carry algorithm names — structural, comments can't match
|
|
183
|
+
literals = string_literals(content, lang)
|
|
184
|
+
for value, line_num in literals:
|
|
185
|
+
lowered = value.strip().lower()
|
|
186
|
+
if lowered in self._ALGO_SEVERITY:
|
|
187
|
+
severity, message = self._ALGO_SEVERITY[lowered]
|
|
188
|
+
findings.append(
|
|
189
|
+
self._make_finding(
|
|
190
|
+
"jwt_weak_algorithm",
|
|
191
|
+
severity,
|
|
192
|
+
rel_path,
|
|
193
|
+
line_num,
|
|
194
|
+
message,
|
|
195
|
+
"Use RS256 or ES256 for distributed systems. Never allow 'none' algorithm.",
|
|
196
|
+
_snippet(line_num),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Hardcoded secrets via assignment targets (all languages)
|
|
201
|
+
try:
|
|
202
|
+
assignments = find_assignments(content, lang)
|
|
203
|
+
except Exception:
|
|
204
|
+
assignments = []
|
|
205
|
+
for assignment in assignments:
|
|
206
|
+
target = str(assignment.get("target", "")).lower()
|
|
207
|
+
value = self._unquote(str(assignment.get("value", "")))
|
|
208
|
+
line_num = int(assignment.get("line", 0) or 0)
|
|
209
|
+
if any(marker in target for marker in self._SECRET_NAMES) and len(value) >= 8:
|
|
210
|
+
findings.append(
|
|
211
|
+
self._make_finding(
|
|
212
|
+
"jwt_hardcoded_secret",
|
|
213
|
+
Severity.CRITICAL,
|
|
214
|
+
rel_path,
|
|
215
|
+
line_num,
|
|
216
|
+
"Hardcoded JWT secret in source code",
|
|
217
|
+
"Move JWT secret to environment variable.",
|
|
218
|
+
_snippet(line_num),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
elif any(marker in target for marker in self._KEY_NAMES) and len(value) >= 16:
|
|
222
|
+
findings.append(
|
|
223
|
+
self._make_finding(
|
|
224
|
+
"jwt_hardcoded_secret",
|
|
225
|
+
Severity.HIGH,
|
|
226
|
+
rel_path,
|
|
227
|
+
line_num,
|
|
228
|
+
"Possible hardcoded cryptographic key",
|
|
229
|
+
"Move JWT secret to environment variable.",
|
|
230
|
+
_snippet(line_num),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Insecure storage: storage calls with JWT-ish string args on the same line
|
|
235
|
+
lits_by_line: dict[int, list[str]] = {}
|
|
236
|
+
for value, line_num in literals:
|
|
237
|
+
lits_by_line.setdefault(line_num, []).append(value.lower())
|
|
238
|
+
storage_calls = find_calls(
|
|
239
|
+
content, lang, {"setItem", "getItem", "cookie", "set_cookie", "setcookie"}
|
|
240
|
+
)
|
|
241
|
+
for call in storage_calls:
|
|
242
|
+
lowered_name = str(call.get("name", "")).lower()
|
|
243
|
+
line_num = int(call.get("line", 0) or 0)
|
|
244
|
+
values = lits_by_line.get(line_num, [])
|
|
245
|
+
if not any(
|
|
246
|
+
v.startswith(prefix) for v in values for prefix in self._STORAGE_PREFIXES
|
|
247
|
+
):
|
|
248
|
+
continue
|
|
249
|
+
if "localstorage" in lowered_name:
|
|
250
|
+
findings.append(
|
|
251
|
+
self._make_finding(
|
|
252
|
+
"jwt_insecure_storage",
|
|
253
|
+
Severity.HIGH,
|
|
254
|
+
rel_path,
|
|
255
|
+
line_num,
|
|
256
|
+
"JWT stored in localStorage — vulnerable to XSS",
|
|
257
|
+
"Use httpOnly, secure cookies for JWT storage.",
|
|
258
|
+
_snippet(line_num),
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
elif "sessionstorage" in lowered_name:
|
|
262
|
+
findings.append(
|
|
263
|
+
self._make_finding(
|
|
264
|
+
"jwt_insecure_storage",
|
|
265
|
+
Severity.MEDIUM,
|
|
266
|
+
rel_path,
|
|
267
|
+
line_num,
|
|
268
|
+
"JWT stored in sessionStorage — vulnerable to XSS",
|
|
269
|
+
"Use httpOnly, secure cookies for JWT storage.",
|
|
270
|
+
_snippet(line_num),
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
elif "cookie" in lowered_name:
|
|
274
|
+
findings.append(
|
|
275
|
+
self._make_finding(
|
|
276
|
+
"jwt_insecure_storage",
|
|
277
|
+
Severity.LOW,
|
|
278
|
+
rel_path,
|
|
279
|
+
line_num,
|
|
280
|
+
"JWT in cookie — verify httpOnly and secure flags are set",
|
|
281
|
+
"Use httpOnly, secure cookies for JWT storage.",
|
|
282
|
+
_snippet(line_num),
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Missing validation: decode sites without verify / algorithms
|
|
287
|
+
decode_calls = find_calls(content, lang, {"decode"})
|
|
288
|
+
if decode_calls:
|
|
289
|
+
verify_calls = find_calls(
|
|
290
|
+
content, lang, {"verify", "authenticate", "validate"}
|
|
291
|
+
)
|
|
292
|
+
if not verify_calls:
|
|
293
|
+
first = decode_calls[0]
|
|
294
|
+
findings.append(
|
|
295
|
+
self._make_finding(
|
|
296
|
+
"jwt_missing_validation",
|
|
297
|
+
Severity.HIGH,
|
|
298
|
+
rel_path,
|
|
299
|
+
int(first.get("line", 0) or 0),
|
|
300
|
+
"JWT decoded without signature verification",
|
|
301
|
+
"Always verify JWT signatures and specify allowed algorithms.",
|
|
302
|
+
_snippet(int(first.get("line", 0) or 0)),
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
for call in decode_calls:
|
|
306
|
+
if not self._call_has_algorithms(content, rel_path, call):
|
|
307
|
+
findings.append(
|
|
308
|
+
self._make_finding(
|
|
309
|
+
"jwt_missing_validation",
|
|
310
|
+
Severity.MEDIUM,
|
|
311
|
+
rel_path,
|
|
312
|
+
int(call.get("line", 0) or 0),
|
|
313
|
+
"JWT decode missing explicit algorithms parameter",
|
|
314
|
+
"Always verify JWT signatures and specify allowed algorithms.",
|
|
315
|
+
_snippet(int(call.get("line", 0) or 0)),
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Check for missing expiration (if JWT is used but no expiry marker found)
|
|
320
|
+
if has_jwt and not self._has_expiry(content, lang, rel_path, assignments):
|
|
321
|
+
findings.append(
|
|
322
|
+
make_finding(
|
|
323
|
+
agent=self.name,
|
|
324
|
+
finding_type="jwt_no_expiry",
|
|
325
|
+
severity=Severity.HIGH,
|
|
326
|
+
file=rel_path,
|
|
327
|
+
message="JWT used but no expiration claim (exp) found in file",
|
|
328
|
+
suggestion="Always set expiration on JWT tokens. Recommended: short-lived access tokens (15min) + refresh tokens.",
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return findings
|
|
333
|
+
|
|
334
|
+
def _call_has_algorithms(self, content: str, rel_path: str, call: dict) -> bool:
|
|
335
|
+
"""True when a decode call passes algorithms (kwarg, object key, or span)."""
|
|
336
|
+
line_no = int(call.get("line", 0) or 0)
|
|
337
|
+
if rel_path.endswith(".py"):
|
|
338
|
+
try:
|
|
339
|
+
import ast as _ast
|
|
340
|
+
|
|
341
|
+
tree = _ast.parse(content)
|
|
342
|
+
for node in _ast.walk(tree):
|
|
343
|
+
if not isinstance(node, _ast.Call):
|
|
344
|
+
continue
|
|
345
|
+
if getattr(node, "lineno", 0) != line_no:
|
|
346
|
+
continue
|
|
347
|
+
if any((kw.arg or "") == "algorithms" for kw in node.keywords):
|
|
348
|
+
return True
|
|
349
|
+
except SyntaxError:
|
|
350
|
+
pass
|
|
351
|
+
return False
|
|
352
|
+
try:
|
|
353
|
+
lang_key = lang_for_file(rel_path)
|
|
354
|
+
for site in js_calls(parse_js(content, lang_key), lang_key):
|
|
355
|
+
if site.line == line_no and "algorithms" in site.text:
|
|
356
|
+
return True
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
return "algorithms" in str(call.get("full_text", ""))
|
|
360
|
+
|
|
361
|
+
def _has_expiry(
|
|
362
|
+
self, content: str, lang: Lang, rel_path: str, assignments: list[dict]
|
|
363
|
+
) -> bool:
|
|
364
|
+
"""Expiry markers via assignment targets, kwargs, and object keys."""
|
|
365
|
+
for assignment in assignments:
|
|
366
|
+
if str(assignment.get("target", "")).lower() in self._EXPIRY_NAMES:
|
|
367
|
+
return True
|
|
368
|
+
if lang == Lang.PYTHON:
|
|
369
|
+
try:
|
|
370
|
+
import ast as _ast
|
|
371
|
+
|
|
372
|
+
tree = _ast.parse(content)
|
|
373
|
+
for node in _ast.walk(tree):
|
|
374
|
+
if isinstance(node, _ast.Call):
|
|
375
|
+
if any((kw.arg or "").lower() in self._EXPIRY_NAMES for kw in node.keywords):
|
|
376
|
+
return True
|
|
377
|
+
elif isinstance(node, _ast.Dict):
|
|
378
|
+
for key in node.keys:
|
|
379
|
+
if isinstance(key, _ast.Constant) and str(key.value).lower() in self._EXPIRY_NAMES:
|
|
380
|
+
return True
|
|
381
|
+
except SyntaxError:
|
|
382
|
+
pass
|
|
383
|
+
return False
|
|
384
|
+
try:
|
|
385
|
+
lang_key = lang_for_file(rel_path)
|
|
386
|
+
props = js_property_names(parse_js(content, lang_key), lang_key)
|
|
387
|
+
return any(p.lower() in self._EXPIRY_NAMES for p in props)
|
|
388
|
+
except Exception:
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
def _make_finding(
|
|
392
|
+
self,
|
|
393
|
+
finding_type: str,
|
|
394
|
+
severity: Severity,
|
|
395
|
+
file: str,
|
|
396
|
+
line: int,
|
|
397
|
+
message: str,
|
|
398
|
+
suggestion: str,
|
|
399
|
+
snippet: str,
|
|
400
|
+
) -> Finding:
|
|
401
|
+
return make_finding(
|
|
402
|
+
agent=self.name,
|
|
403
|
+
finding_type=finding_type,
|
|
404
|
+
severity=severity,
|
|
405
|
+
file=file,
|
|
406
|
+
line_start=line,
|
|
407
|
+
message=message,
|
|
408
|
+
suggestion=suggestion,
|
|
409
|
+
code_snippet=snippet[:120],
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def _check_no_jwt_in_codebase(self, content: str, rel_path: str) -> list[Finding]:
|
|
413
|
+
findings = []
|
|
414
|
+
# KEY=VALUE lines need splitting, not patterns (env-style parsing).
|
|
415
|
+
for line_num, line in enumerate(content.splitlines(), 1):
|
|
416
|
+
if "=" not in line:
|
|
417
|
+
continue
|
|
418
|
+
name, _, value = line.partition("=")
|
|
419
|
+
name, value = name.strip().upper(), value.strip().strip("\"'")
|
|
420
|
+
if not value:
|
|
421
|
+
continue
|
|
422
|
+
if name == "JWT_SECRET":
|
|
423
|
+
severity = Severity.CRITICAL
|
|
424
|
+
message = "JWT secret in environment/config file — use .env, not hardcoded"
|
|
425
|
+
elif name == "JWT_KEY":
|
|
426
|
+
severity = Severity.HIGH
|
|
427
|
+
message = "JWT key in environment/config file — ensure .env is in .gitignore"
|
|
428
|
+
else:
|
|
429
|
+
continue
|
|
430
|
+
findings.append(
|
|
431
|
+
self._make_finding(
|
|
432
|
+
"jwt_env_exposure",
|
|
433
|
+
severity,
|
|
434
|
+
rel_path,
|
|
435
|
+
line_num,
|
|
436
|
+
message,
|
|
437
|
+
"Ensure .env files are in .gitignore. Use secret managers in production.",
|
|
438
|
+
line.strip(),
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
return findings
|
|
442
|
+
|
|
443
|
+
def _should_skip(self, rel_path: str) -> bool:
|
|
444
|
+
parts = rel_path.replace("\\", "/").split("/")
|
|
445
|
+
return any(p in self._SKIP_DIRS for p in parts)
|
|
446
|
+
|
|
447
|
+
def _safe_rglob(self, root: Path, pattern: str):
|
|
448
|
+
suffix = pattern.replace("*.", ".")
|
|
449
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
450
|
+
dirnames[:] = [d for d in dirnames if d not in self._SKIP_DIRS]
|
|
451
|
+
for fn in filenames:
|
|
452
|
+
if fn.endswith(suffix):
|
|
453
|
+
yield Path(dirpath) / fn
|