From ba99d3237f10948f5b6d6f5f70c2e64b82ea468d Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:24:54 +0800 Subject: [PATCH 01/12] feat: enforce engagement authorization framing --- .env.example | 13 + docs/configuration/ENVIRONMENT_VARIABLES.md | 27 +++ .../agents/advanced_exploitation_agent.py | 21 +- pentestgpt/agents/agent_abstract.py | 40 ++++ pentestgpt/agents/command_generator.py | 15 +- pentestgpt/agents/exploit_developer.py | 39 ++- pentestgpt/agents/opsec_agent.py | 83 ++++++- pentestgpt/agents/repetition_detector.py | 2 +- pentestgpt/agents/results_verifier.py | 30 ++- pentestgpt/agents/stealth_operations_agent.py | 8 +- pentestgpt/agents/strategy_agent.py | 40 +++- pentestgpt/api/stealth_api.py | 4 +- pentestgpt/config/settings.py | 8 + pentestgpt/core/engagement.py | 226 ++++++++++++++++++ pentestgpt/core/exceptions.py | 17 ++ pentestgpt/core/scope_guard.py | 71 ++++++ .../detection_resistance/evasion_engine.py | 12 +- .../effectiveness/success_rate_analytics.py | 9 +- .../validation_confirmation_system.py | 6 +- pentestgpt/experimental/_mitre_prototype.py | 3 +- pentestgpt/llms/router.py | 8 +- pentestgpt/monitoring/alert_correlation.py | 6 +- .../monitoring/detection_intelligence.py | 6 +- pentestgpt/monitoring/detection_monitor.py | 6 +- .../monitoring/security_event_analyzer.py | 6 +- pentestgpt/orchestrator.py | 38 ++- pentestgpt/prompts/authorization.py | 80 +++++++ pentestgpt/prompts/mitre_prompts.py | 17 +- pentestgpt/prompts/prompt_class.py | 52 ++-- pentestgpt/report_generator.py | 6 +- pentestgpt/utils/dependency_injection.py | 24 +- pentestgpt/utils/orchestrator_factory.py | 32 +++ .../utils/strategy_adaptation_controller.py | 6 +- tests/agents/test_exploit_scope.py | 72 ++++++ tests/agents/test_opsec_agent.py | 18 +- tests/api/test_opsec_session_scope.py | 36 +++ tests/core/test_engagement.py | 98 ++++++++ tests/core/test_scope_guard.py | 65 +++++ .../integration/test_scope_guard_lifecycle.py | 70 ++++++ tests/llms/test_router_cache_isolation.py | 41 ++++ tests/prompts/test_authorization_preamble.py | 49 ++++ tests/prompts/test_prompt_templates.py | 5 + tests/prompts/test_route_task_coverage.py | 49 ++++ tests/regression/test_critical_fixes.py | 10 +- tests/regression/test_fixes_quick.py | 2 +- tests/stealth/test_stealth_components.py | 5 +- tests/utils/test_orchestrator_factory.py | 28 ++- 47 files changed, 1408 insertions(+), 101 deletions(-) create mode 100644 pentestgpt/core/engagement.py create mode 100644 pentestgpt/core/scope_guard.py create mode 100644 pentestgpt/prompts/authorization.py create mode 100644 tests/agents/test_exploit_scope.py create mode 100644 tests/api/test_opsec_session_scope.py create mode 100644 tests/core/test_engagement.py create mode 100644 tests/core/test_scope_guard.py create mode 100644 tests/integration/test_scope_guard_lifecycle.py create mode 100644 tests/llms/test_router_cache_isolation.py create mode 100644 tests/prompts/test_authorization_preamble.py create mode 100644 tests/prompts/test_route_task_coverage.py diff --git a/.env.example b/.env.example index 607f2eb..b04d86b 100644 --- a/.env.example +++ b/.env.example @@ -65,6 +65,19 @@ RATE_LIMIT_WINDOW=3600 # Logging LOG_LEVEL=INFO +# ----------------------------------------------------------------------------- +# Engagement Authorization Scope (optional; JSON arrays) +# ----------------------------------------------------------------------------- + +# Leave both target lists empty for neutral prompt framing. When either list is +# configured, every effective target must validate in-scope before generation +# or execution. +ENGAGEMENT_CLIENT= +ENGAGEMENT_ID= +AUTHORIZED_HOSTS=[] +AUTHORIZED_IP_RANGES=[] +RULES_OF_ENGAGEMENT= + # ----------------------------------------------------------------------------- # Telemetry & Monitoring (Langfuse) # ----------------------------------------------------------------------------- diff --git a/docs/configuration/ENVIRONMENT_VARIABLES.md b/docs/configuration/ENVIRONMENT_VARIABLES.md index 35d33fc..dd52270 100644 --- a/docs/configuration/ENVIRONMENT_VARIABLES.md +++ b/docs/configuration/ENVIRONMENT_VARIABLES.md @@ -67,6 +67,33 @@ DATABASE_URL=postgresql://user:password@localhost:5432/pentestgpt | `PENTESTGPT_USER_USER` | Default user username | `user` | | `PENTESTGPT_USER_PASSWORD` | Default user password | Change from default | +## Engagement Authorization Scope + +These optional settings feed the canonical `EngagementScope` used by both prompt framing and the mandatory scope guard. +List values use JSON syntax in `.env` files. + +| Variable | Description | Default | +| ---------------------- | ------------------------------------------------ | ------- | +| `ENGAGEMENT_CLIENT` | Client name included in validated prompt context | — | +| `ENGAGEMENT_ID` | Engagement identifier included in audit records | — | +| `AUTHORIZED_HOSTS` | JSON array of authorized hostnames or exact IPs | `[]` | +| `AUTHORIZED_IP_RANGES` | JSON array of authorized CIDR ranges | `[]` | +| `RULES_OF_ENGAGEMENT` | Optional rules-of-engagement notes | — | + +Example: + +```env +ENGAGEMENT_CLIENT=Example Corp +ENGAGEMENT_ID=PT-2026-014 +AUTHORIZED_HOSTS=["lab.example.test"] +AUTHORIZED_IP_RANGES=["10.20.0.0/16"] +RULES_OF_ENGAGEMENT=Testing window 09:00-17:00 UTC +``` + +When both target lists are empty, PentestGPT remains usable for local/lab workflows but emits neutral framing and does +not claim authorization. Once either list is configured, a missing or out-of-scope effective target blocks LLM +generation and tool execution. Scope decisions are appended to `scope_audit.jsonl` in the session directory. + ### OAuth2 (optional) | Variable | Description | Default | diff --git a/pentestgpt/agents/advanced_exploitation_agent.py b/pentestgpt/agents/advanced_exploitation_agent.py index 8db1cf1..2a0ac8b 100644 --- a/pentestgpt/agents/advanced_exploitation_agent.py +++ b/pentestgpt/agents/advanced_exploitation_agent.py @@ -21,6 +21,9 @@ from loguru import logger +from pentestgpt.core.engagement import EngagementScope, extract_effective_targets +from pentestgpt.core.scope_guard import ScopeGuard +from pentestgpt.prompts.authorization import EXPLOIT_DEVELOPER_ROLE, build_system_prompt from pentestgpt.utils.model_router import ModelRouter, TaskType from .exploit_developer import ExploitDeveloper @@ -89,6 +92,8 @@ def __init__( self.agent_id = agent_id self.router = model_router self.tool_executor = tool_executor + self._engagement_scope: EngagementScope | None = None + self._scope_guard: ScopeGuard | None = None # Initialize components self.knowledge_manager = KnowledgeManager() @@ -118,6 +123,12 @@ def __init__( logger.info(f"AdvancedExploitationAgent initialized: {self.agent_id}") + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Inject scope state and forward it through the exploitation chain.""" + self._engagement_scope = scope + self._scope_guard = guard + self.exploit_developer.set_engagement_scope(scope, guard) + def create_exploitation_plan( self, target: str, @@ -155,11 +166,17 @@ def create_exploitation_plan( "Output as JSON with detailed exploitation procedures." ) - system_prompt = "You are an expert in penetration testing and exploit development." + targets = extract_effective_targets(target, reconnaissance_data) + if self._scope_guard is not None: + self._scope_guard.check("exploitation_planning", targets) response, _provider, _cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, plan_prompt, - system_prompt=system_prompt, + system_prompt=build_system_prompt( + EXPLOIT_DEVELOPER_ROLE, + self._engagement_scope, + targets, + ), ) try: diff --git a/pentestgpt/agents/agent_abstract.py b/pentestgpt/agents/agent_abstract.py index d252343..d5cb8e8 100644 --- a/pentestgpt/agents/agent_abstract.py +++ b/pentestgpt/agents/agent_abstract.py @@ -8,6 +8,10 @@ from dataclasses import dataclass, field from typing import Any, Protocol +from pentestgpt.core.engagement import EngagementScope, extract_effective_targets +from pentestgpt.core.scope_guard import ScopeGuard +from pentestgpt.prompts.authorization import build_system_prompt + # Agent Abstract Constants class AgentAbstractConstants: @@ -96,9 +100,25 @@ class BaseAgent(ABC): def __init__(self, **kwargs: Any): """Initialize the base agent.""" + self._engagement_scope: EngagementScope | None = None + self._scope_guard: ScopeGuard | None = None self.metadata = self._get_metadata() self._initialize_dependencies(**kwargs) + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Inject engagement scope state shared across the orchestrator lifecycle.""" + self._engagement_scope = scope + self._scope_guard = guard + + def _build_system_prompt(self, role: str, phase: str, *contexts: Any) -> str: + """Validate targets and build an authorization-aware system prompt.""" + targets = extract_effective_targets(*contexts) + if not targets and self._scope_guard is not None: + targets = set(self._scope_guard.last_allowed_targets) + if self._scope_guard is not None: + self._scope_guard.check(phase, targets) + return build_system_prompt(role, self._engagement_scope, targets) + @abstractmethod def _get_metadata(self) -> AgentMetadata: """Get agent metadata. @@ -139,6 +159,10 @@ class StrategyAgentInterface(Protocol): ResultsVerifierInterface, and ToolExecutorInterface. """ + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Inject the engagement scope and mandatory guard.""" + ... + def plan_next_phase( self, current_tactic: Any, @@ -323,6 +347,10 @@ class CommandGeneratorInterface(Protocol): with RAG and MITRE integration. """ + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Inject the engagement scope and mandatory guard.""" + ... + def generate_commands( self, task: Any, @@ -351,6 +379,10 @@ class RepetitionDetectorInterface(Protocol): to prevent loops in agent execution. """ + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Inject the engagement scope and mandatory guard.""" + ... + similarity_threshold: float def is_repetitive(self, task: Any, ptt: Any) -> tuple[bool, float, list[Any]]: @@ -373,6 +405,10 @@ class ResultsVerifierInterface(Protocol): and detecting errors or unexpected outcomes. """ + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Inject the engagement scope and mandatory guard.""" + ... + def verify_commands( self, commands: list[str], @@ -419,6 +455,10 @@ class ToolExecutorInterface(Protocol): with sandboxing and security measures. """ + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Inject the engagement scope and mandatory guard.""" + ... + def execute(self, command: str, timeout: int | None = None) -> str: """Execute a single command. diff --git a/pentestgpt/agents/command_generator.py b/pentestgpt/agents/command_generator.py index f90e758..4624359 100644 --- a/pentestgpt/agents/command_generator.py +++ b/pentestgpt/agents/command_generator.py @@ -15,6 +15,7 @@ from pentestgpt.agents.parsers.registry import ParserRegistry from pentestgpt.llms.router import ModelRouter, TaskType from pentestgpt.mitre import MITRETechnique +from pentestgpt.prompts.authorization import MITRE_PENTEST_ROLE from pentestgpt.prompts.mitre_prompts import MITREPrompts from pentestgpt.prompts.prompt_class_v3 import PentestGPTPromptV3 from pentestgpt.utils.learning_context_manager import redact_mapping @@ -22,7 +23,7 @@ from .agent_abstract import AgentMetadata, BaseAgent, CommandGeneratorInterface -class CommandGenerator(CommandGeneratorInterface, BaseAgent): +class CommandGenerator(BaseAgent, CommandGeneratorInterface): """Command Generator with RAG and MITRE integration. Uses GPT-4o for command generation with RAG-retrieved context @@ -182,7 +183,11 @@ def generate_commands( response, provider, cost = self.router.route_task( TaskType.COMMAND_GENERATION, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "command_generation", + target_info, + ), ) # Parse response (handles JSON errors internally and falls back to text parsing) @@ -570,7 +575,11 @@ def refine_command( response, provider, cost = self.router.route_task( TaskType.ERROR_RECOVERY, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "command_refinement", + target_info, + ), ) data = json.loads(response) corrected = data.get("corrected_command", command) diff --git a/pentestgpt/agents/exploit_developer.py b/pentestgpt/agents/exploit_developer.py index 519e271..597f909 100644 --- a/pentestgpt/agents/exploit_developer.py +++ b/pentestgpt/agents/exploit_developer.py @@ -13,6 +13,11 @@ from loguru import logger +from pentestgpt.core.engagement import EngagementScope, extract_effective_targets +from pentestgpt.core.exceptions import OutOfScopeError +from pentestgpt.core.scope_guard import ScopeGuard +from pentestgpt.prompts.authorization import EXPLOIT_DEVELOPER_ROLE, build_system_prompt + class ExploitType(Enum): """Types of exploits.""" @@ -101,11 +106,25 @@ def __init__( self.payload_library = payload_library self.model_router = model_router self.tool_executor = tool_executor + self._engagement_scope: EngagementScope | None = None + self._scope_guard: ScopeGuard | None = None self.exploit_candidates: dict[str, ExploitCandidate] = {} self.exploitation_results: list[ExploitationResult] = [] logger.info("ExploitDeveloper initialized") + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Inject engagement scope for LLM generation and exploit execution.""" + self._engagement_scope = scope + self._scope_guard = guard + + def _authorization_system_prompt(self, phase: str, target: str) -> str: + """Validate one exploit target and render its system prompt.""" + targets = extract_effective_targets(target) + if self._scope_guard is not None: + self._scope_guard.check(phase, targets) + return build_system_prompt(EXPLOIT_DEVELOPER_ROLE, self._engagement_scope, targets) + def develop_and_test_exploits( self, vulnerabilities: list[dict[str, Any]], @@ -318,8 +337,14 @@ def _generate_exploit_code( code, _provider, _cost = self.model_router.route_task( TaskType.COMMAND_GENERATION, prompt, + system_prompt=self._authorization_system_prompt( + "exploit_generation", + target, + ), ) return code or self._static_fallback(exploit_type) + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error generating exploit code: {e}") return self._static_fallback(exploit_type) @@ -362,7 +387,7 @@ def _build_exploit_prompt( lport = str(vulnerability.get("lport") or self.payload_library.get("lport") or 4444) lines: list[str] = [ - "You are an expert exploit developer working an authorized penetration test.", + "You are an expert exploit developer working on the supplied penetration-testing task.", f"Target host: {target}", f"Exploit type: {exploit_type.value}", f"Vulnerability name: {vulnerability.get('name', 'unknown')}", @@ -447,7 +472,7 @@ def _generate_payload(self, payload_type: PayloadType, target: str) -> str: from pentestgpt.llms.router import TaskType prompt = ( - "You are an expert exploit developer working an authorized penetration test.\n" + "You are an expert exploit developer working on the supplied penetration-testing task.\n" f"Target host: {target}\n" f"Payload type: {payload_type.value}\n" "Output ONLY the raw payload, no commentary or fences." @@ -455,8 +480,14 @@ def _generate_payload(self, payload_type: PayloadType, target: str) -> str: payload, _provider, _cost = self.model_router.route_task( TaskType.COMMAND_GENERATION, prompt, + system_prompt=self._authorization_system_prompt( + "payload_generation", + target, + ), ) return payload or self._static_payload_fallback(payload_type) + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error generating payload: {e}") return self._static_payload_fallback(payload_type) @@ -540,6 +571,8 @@ def _test_exploit( "shell_access": False, "privilege_level": "unknown", } + if self._scope_guard is not None: + self._scope_guard.check("exploit_execution", extract_effective_targets(target)) output = self.tool_executor.execute(candidate.exploit_code, timeout=120) # str markers = ("uid=", "root@", "SYSTEM", "/etc/passwd", "Microsoft Windows") success = any(m in output for m in markers) @@ -552,6 +585,8 @@ def _test_exploit( "privilege_level": ("root" if privileged else "user") if success else "unknown", } + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error testing exploit: {e}") return { diff --git a/pentestgpt/agents/opsec_agent.py b/pentestgpt/agents/opsec_agent.py index 0fc86fc..f4b1433 100644 --- a/pentestgpt/agents/opsec_agent.py +++ b/pentestgpt/agents/opsec_agent.py @@ -9,10 +9,12 @@ """ import hashlib +import ipaddress import json import os import platform from datetime import datetime +from pathlib import Path from typing import Any from uuid import uuid4 @@ -24,6 +26,13 @@ StealthConfig, StealthLevel, ) +from pentestgpt.core.engagement import EngagementScope, extract_effective_targets +from pentestgpt.core.exceptions import OutOfScopeError +from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard +from pentestgpt.prompts.authorization import ( + OPSEC_PENTEST_ROLE, + OPSEC_RISK_ASSESSMENT_ROLE, +) from pentestgpt.utils.model_router import ModelRouter, TaskType from .agent_abstract import AgentMetadata, BaseAgent @@ -252,12 +261,23 @@ def _check_scope_validity(self, context: dict[str, Any]) -> tuple[bool, str]: if not scope.get("targets") and not scope.get("ip_ranges"): return False, "No target scope defined" - # Check if all targets are within scope - if targets and scope.get("targets"): - authorized_targets = scope["targets"] - for target in targets: - if target not in authorized_targets: - return False, f"Target {target} not in authorized scope" + # Use the canonical scope model so exact hosts and CIDR members share + # the same normalization and all-in-scope semantics as ScopeGuard. + if targets: + authorized_hosts: list[str] = [] + authorized_ranges = list(scope.get("ip_ranges", [])) + for authorized_target in scope.get("targets", []): + try: + authorized_ranges.append(str(ipaddress.ip_network(authorized_target, strict=False))) + except ValueError: + authorized_hosts.append(str(authorized_target)) + engagement_scope = EngagementScope( + authorized_hosts=frozenset(authorized_hosts), + authorized_ip_ranges=tuple(authorized_ranges), + ) + decision = engagement_scope.validate_targets({str(target) for target in targets}) + if not decision.allowed: + return False, decision.reason return True, "Scope validity confirmed" @@ -307,10 +327,16 @@ def generate_audit_report(self) -> dict[str, Any]: class OPSECSession: """OPSEC session management.""" - def __init__(self, session_id: str, config: OPSECConfig): + def __init__( + self, + session_id: str, + config: OPSECConfig, + engagement_scope: EngagementScope | None = None, + ): """Initialize OPSECSession.""" self.session_id = session_id self.config = config + self.engagement_scope = engagement_scope self.start_time = datetime.now() self.last_activity = datetime.now() self.isolated = config.session_isolation @@ -486,7 +512,7 @@ def get_health_status(self) -> dict[str, Any]: """Get agent health status.""" return {"status": "healthy", "is_healthy": True} - def create_session(self) -> str: + def create_session(self, scope: EngagementScope | None = None) -> str: """Create a new OPSEC session. Returns: @@ -495,7 +521,16 @@ def create_session(self) -> str: session_id = str(uuid4())[:8] # Short ID for display # Create session with current configuration - session = OPSECSession(session_id, self.config.opsec) + if scope is not None: + self._engagement_scope = scope + if self._scope_guard is None: + self._scope_guard = ScopeGuard( + self._engagement_scope, + JsonlAuditSink(Path("sessions") / session_id / "scope_audit.jsonl"), + ) + elif self._scope_guard.scope != self._engagement_scope: + self._scope_guard = ScopeGuard(self._engagement_scope, self._scope_guard.audit_sink) + session = OPSECSession(session_id, self.config.opsec, self._engagement_scope) session.start_session() self.current_session = session @@ -550,7 +585,11 @@ def analyze_target_environment(self, target_info: dict[str, Any]) -> dict[str, A response, provider, cost = self.router.route_task( TaskType.VULNERABILITY_ANALYSIS, analysis_prompt, - system_prompt="You are a security expert specializing in operational security and risk assessment.", + system_prompt=self._build_system_prompt( + OPSEC_RISK_ASSESSMENT_ROLE, + "opsec_target_analysis", + target_info, + ), ) analysis: dict[str, Any] = json.loads(response) @@ -655,10 +694,26 @@ def validate_operation( """ logger.info(f"Validating operation: {operation}") + targets = extract_effective_targets(context) + if self._scope_guard is not None: + try: + self._scope_guard.check(f"opsec_{operation}", targets) + except OutOfScopeError as exc: + return False, str(exc) + elif self._engagement_scope is not None: + decision = self._engagement_scope.validate_targets(targets) + if not decision.allowed: + return False, decision.reason + if not self.current_session: logger.warning("No active OPSEC session for operation validation") - return True, "No session active" + return False, "No active OPSEC session; this check does not establish authorization" + if self._engagement_scope is not None: + context = { + **context, + **self._engagement_scope.to_opsec_context(sorted(targets)), + } return self.current_session.validate_operation(operation, context) def log_activity(self, activity_type: str, details: dict[str, Any]) -> None: @@ -764,7 +819,11 @@ def get_opsec_recommendations( response, _provider, cost = self.router.route_task( TaskType.VULNERABILITY_ANALYSIS, prompt, - system_prompt="You are an expert in operational security and penetration testing OPSEC.", + system_prompt=self._build_system_prompt( + OPSEC_PENTEST_ROLE, + "opsec_recommendations", + target_info, + ), ) recommendations: dict[str, Any] = json.loads(response) diff --git a/pentestgpt/agents/repetition_detector.py b/pentestgpt/agents/repetition_detector.py index 30d3446..4975aa6 100644 --- a/pentestgpt/agents/repetition_detector.py +++ b/pentestgpt/agents/repetition_detector.py @@ -29,7 +29,7 @@ from .agent_abstract import AgentMetadata, BaseAgent, RepetitionDetectorInterface -class RepetitionDetector(RepetitionDetectorInterface, BaseAgent): +class RepetitionDetector(BaseAgent, RepetitionDetectorInterface): """Repetition Detector using semantic similarity analysis. Uses TF-IDF vectorization and cosine similarity to detect diff --git a/pentestgpt/agents/results_verifier.py b/pentestgpt/agents/results_verifier.py index 7193df4..76a8646 100644 --- a/pentestgpt/agents/results_verifier.py +++ b/pentestgpt/agents/results_verifier.py @@ -16,12 +16,16 @@ from pentestgpt.agents.parsers.registry import ParserRegistry from pentestgpt.llms.router import ModelRouter, TaskType from pentestgpt.mitre import MITRETechnique +from pentestgpt.prompts.authorization import ( + MITRE_PENTEST_ROLE, + SECURITY_OUTPUT_PARSER_ROLE, +) from pentestgpt.prompts.mitre_prompts import MITREPrompts from .agent_abstract import AgentMetadata, BaseAgent, ResultsVerifierInterface -class ResultsVerifier(ResultsVerifierInterface, BaseAgent): +class ResultsVerifier(BaseAgent, ResultsVerifierInterface): """Results Verifier for proactive validation and error detection. Uses GPT-4o for complex analysis, GPT-3.5 for simple parsing @@ -96,7 +100,11 @@ def verify_commands( response, provider, cost = self.router.route_task( TaskType.ERROR_RECOVERY, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "command_verification", + commands, + ), ) except (TimeoutError, FuturesTimeoutError): # Timeouts should propagate so callers/tests can handle them explicitly. @@ -248,7 +256,12 @@ def analyze_results( response, provider, cost = self.router.route_task( TaskType.VULNERABILITY_ANALYSIS, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "result_analysis", + commands, + results, + ), ) analysis = json.loads(response) # type: dict[str, Any] @@ -533,7 +546,11 @@ def suggest_fixes( response, _provider, cost = self.router.route_task( TaskType.ERROR_RECOVERY, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "fix_suggestion", + target_info, + ), ) fixes = json.loads(response) # type: list[dict[str, Any]] @@ -578,7 +595,10 @@ def parse_tool_output( response, provider, cost = self.router.route_task( TaskType.OUTPUT_PARSING, prompt, - system_prompt="You are an expert at parsing security tool outputs.", + system_prompt=self._build_system_prompt( + SECURITY_OUTPUT_PARSER_ROLE, + "tool_output_parsing", + ), ) parsed = json.loads(response) # type: dict[str, Any] diff --git a/pentestgpt/agents/stealth_operations_agent.py b/pentestgpt/agents/stealth_operations_agent.py index 5b604d8..1a9c727 100644 --- a/pentestgpt/agents/stealth_operations_agent.py +++ b/pentestgpt/agents/stealth_operations_agent.py @@ -32,6 +32,7 @@ TimingEvasion, ) from pentestgpt.config.stealth_config import StealthConfig +from pentestgpt.prompts.authorization import OPSEC_PENTEST_ROLE from pentestgpt.utils.model_router import ModelRouter, TaskType _STEALTH_TO_EVASION: dict[str, str] = { @@ -409,7 +410,12 @@ def create_opsec_plan( response, _provider, _cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, plan_prompt, - system_prompt="You are an expert in operational security and penetration testing OPSEC.", + system_prompt=self._build_system_prompt( + OPSEC_PENTEST_ROLE, + "opsec_plan_generation", + target_environment, + scope, + ), ) try: diff --git a/pentestgpt/agents/strategy_agent.py b/pentestgpt/agents/strategy_agent.py index b25663b..570596a 100644 --- a/pentestgpt/agents/strategy_agent.py +++ b/pentestgpt/agents/strategy_agent.py @@ -15,13 +15,14 @@ from pentestgpt.mitre import MITREGuidedPlanner, MITRETactic, get_technique from pentestgpt.mitre.taxonomy import MITRE_TECHNIQUES +from pentestgpt.prompts.authorization import MITRE_PENTEST_ROLE from pentestgpt.prompts.mitre_prompts import MITREPrompts from pentestgpt.utils.model_router import ModelRouter, TaskType from .agent_abstract import AgentMetadata, BaseAgent, StrategyAgentInterface -class StrategyAgent(StrategyAgentInterface, BaseAgent): +class StrategyAgent(BaseAgent, StrategyAgentInterface): """Strategy Agent for high-level planning and PTT management. Uses GPT-4o or Claude Sonnet for complex strategic reasoning @@ -133,7 +134,12 @@ def plan_next_phase( response, provider, cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "strategy_planning", + target_info, + findings, + ), ) try: @@ -186,7 +192,11 @@ def update_ptt_with_findings(self, ptt_state: str, findings: dict[str, Any]) -> response, provider, cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "ptt_update", + findings, + ), ) logger.info(f"PTT updated (cost: ${cost:.4f}, provider: {provider})") @@ -260,7 +270,11 @@ def prioritize_tasks( response, provider, cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "task_prioritization", + target_info, + ), ) prioritized = json.loads(response) # type: list[dict[str, Any]] @@ -316,7 +330,11 @@ def suggest_alternative_technique( response, provider, cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "alternative_technique", + target_info, + ), ) result = json.loads(response) # type: dict[str, Any] @@ -359,7 +377,11 @@ def assess_attack_surface(self, target_info: dict[str, Any]) -> dict[str, Any]: response, provider, cost = self.router.route_task( TaskType.VULNERABILITY_ANALYSIS, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "attack_surface_assessment", + target_info, + ), ) assessment = json.loads(response) # type: dict[str, Any] @@ -412,7 +434,11 @@ def generate_attack_path( response, _provider, cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt=MITREPrompts.get_system_prompt(), + system_prompt=self._build_system_prompt( + MITRE_PENTEST_ROLE, + "attack_path_generation", + target_info, + ), ) attack_path = json.loads(response) # type: list[dict[str, Any]] diff --git a/pentestgpt/api/stealth_api.py b/pentestgpt/api/stealth_api.py index 13029ae..703cb4a 100644 --- a/pentestgpt/api/stealth_api.py +++ b/pentestgpt/api/stealth_api.py @@ -28,6 +28,7 @@ StealthConfigManager, StealthLevel, ) +from pentestgpt.core.engagement import EngagementScope from pentestgpt.database.stealth_models import AdaptationAction as DBAdaptationAction from pentestgpt.database.stealth_models import DetectionEvent as DBDetectionEvent from pentestgpt.database.stealth_models import ( @@ -312,7 +313,8 @@ async def create_opsec_session( try: # Create session - session_id = opsec_agent.create_session() + scope = EngagementScope.from_authorized_scope(session_request.authorized_scope) + session_id = opsec_agent.create_session(scope=scope) # Log session creation background_tasks.add_task( diff --git a/pentestgpt/config/settings.py b/pentestgpt/config/settings.py index 484284d..cd67b0f 100644 --- a/pentestgpt/config/settings.py +++ b/pentestgpt/config/settings.py @@ -13,6 +13,7 @@ from __future__ import annotations +from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict @@ -77,6 +78,13 @@ class AppSettings(BaseSettings): log_level: str = "INFO" debug: bool = False + # Engagement authorization (empty target lists mean unconfigured/neutral) + engagement_client: str | None = None + engagement_id: str | None = None + authorized_hosts: list[str] = Field(default_factory=list) + authorized_ip_ranges: list[str] = Field(default_factory=list) + rules_of_engagement: str | None = None + # ------------------------------------------------------------------ # Optional subsystem feature flags # ------------------------------------------------------------------ diff --git a/pentestgpt/core/engagement.py b/pentestgpt/core/engagement.py new file mode 100644 index 0000000..85af769 --- /dev/null +++ b/pentestgpt/core/engagement.py @@ -0,0 +1,226 @@ +"""Canonical engagement authorization scope and target normalization.""" + +from __future__ import annotations + +import ipaddress +import re +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any +from urllib.parse import urlsplit + +if TYPE_CHECKING: + from pentestgpt.config.settings import AppSettings + + +_HOSTNAME_RE = re.compile( + r"^(?=.{1,253}\.?$)(?:[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?\.)*" r"[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?\.?$", + re.IGNORECASE, +) +_TARGET_KEYS = { + "target", + "targets", + "host", + "hosts", + "hostname", + "hostnames", + "ip", + "ips", + "ip_address", + "ip_addresses", + "url", + "urls", + "base_url", + "authorized_hosts", + "discovered_hosts", +} +_CONTEXT_KEYS = {"target_info", "session_findings", "reconnaissance_data", "findings", "structured_findings"} + + +def normalize_target(value: str) -> str: + """Normalize a hostname, IP literal, or URL to a comparable host string. + + Empty strings and values that are not recognizable targets normalize to an + empty string so callers can safely discard them. + """ + candidate = value.strip() + if not candidate: + return "" + + if "://" in candidate or candidate.startswith("//"): + parsed = urlsplit(candidate if "://" in candidate else f"https:{candidate}") + candidate = parsed.hostname or "" + elif candidate.startswith("["): + closing = candidate.find("]") + if closing == -1: + return "" + remainder = candidate[closing + 1 :] + if remainder and not (remainder.startswith(":") and remainder[1:].isdigit()): + return "" + candidate = candidate[1:closing] + elif candidate.count(":") == 1: + host, port = candidate.rsplit(":", 1) + if port.isdigit(): + candidate = host + + candidate = candidate.strip().rstrip(".").casefold() + if not candidate or any(char.isspace() for char in candidate): + return "" + + try: + return ipaddress.ip_address(candidate).compressed.casefold() + except ValueError: + return candidate if _HOSTNAME_RE.fullmatch(candidate) else "" + + +def extract_effective_targets(*contexts: Any) -> set[str]: + """Extract explicit target fields from nested engagement inputs.""" + targets: set[str] = set() + + def add_values(value: Any) -> None: + if isinstance(value, str): + normalized = normalize_target(value) + if normalized: + targets.add(normalized) + return + if isinstance(value, Mapping): + visit_mapping(value) + return + if isinstance(value, Iterable) and not isinstance(value, (bytes, bytearray)): + for item in value: + add_values(item) + + def visit_mapping(mapping: Mapping[Any, Any]) -> None: + for raw_key, value in mapping.items(): + key = str(raw_key).casefold() + if key in _TARGET_KEYS: + add_values(value) + elif key in _CONTEXT_KEYS and isinstance(value, (Mapping, list, tuple, set, frozenset)): + add_values(value) + + for context in contexts: + if isinstance(context, Mapping): + visit_mapping(context) + else: + add_values(context) + return targets + + +@dataclass(frozen=True) +class ScopeDecision: + """Result of validating a set of targets against an engagement scope.""" + + allowed: bool + reason: str + offending: frozenset[str] = frozenset() + + +@dataclass(frozen=True) +class EngagementScope: + """The single authorization model used by prompts and enforcement.""" + + client: str | None = None + engagement_id: str | None = None + authorized_hosts: frozenset[str] = frozenset() + authorized_ip_ranges: tuple[str, ...] = () + roe_notes: str | None = None + + def __post_init__(self) -> None: + normalized_hosts = frozenset( + normalized for host in self.authorized_hosts if (normalized := normalize_target(str(host))) + ) + normalized_ranges = tuple( + dict.fromkeys( + str(ipaddress.ip_network(str(network).strip(), strict=False)) for network in self.authorized_ip_ranges + ) + ) + object.__setattr__(self, "authorized_hosts", normalized_hosts) + object.__setattr__(self, "authorized_ip_ranges", normalized_ranges) + + @classmethod + def from_settings(cls, settings: AppSettings) -> EngagementScope | None: + """Build a configured scope from application settings, if present.""" + scope = cls( + client=settings.engagement_client, + engagement_id=settings.engagement_id, + authorized_hosts=frozenset(settings.authorized_hosts), + authorized_ip_ranges=tuple(settings.authorized_ip_ranges), + roe_notes=settings.rules_of_engagement, + ) + return scope if scope.is_configured() else None + + @classmethod + def from_authorized_scope(cls, data: dict[str, Any]) -> EngagementScope: + """Parse the public API's scope dictionary into the canonical model.""" + raw_hosts = data.get("authorized_hosts", data.get("hosts", data.get("targets", []))) + raw_ranges = data.get( + "authorized_ip_ranges", + data.get("ip_ranges", data.get("networks", [])), + ) + hosts = _as_string_list(raw_hosts) + ranges = _as_string_list(raw_ranges) + return cls( + client=_optional_string(data.get("client")), + engagement_id=_optional_string(data.get("engagement_id")), + authorized_hosts=frozenset(hosts), + authorized_ip_ranges=tuple(ranges), + roe_notes=_optional_string(data.get("roe_notes", data.get("rules_of_engagement"))), + ) + + def is_configured(self) -> bool: + """Return whether this scope has at least one authorized target.""" + return bool(self.authorized_hosts or self.authorized_ip_ranges) + + def validate_targets(self, targets: set[str]) -> ScopeDecision: + """Validate all effective targets, failing closed for configured scope.""" + normalized_targets = {normalized for target in targets if (normalized := normalize_target(target))} + if not self.is_configured(): + return ScopeDecision(True, "No engagement scope configured; authorization is not asserted") + if not normalized_targets: + return ScopeDecision(False, "Configured engagement scope requires at least one effective target") + + networks = tuple(ipaddress.ip_network(network, strict=False) for network in self.authorized_ip_ranges) + offending: set[str] = set() + for target in normalized_targets: + try: + address = ipaddress.ip_address(target) + except ValueError: + if target not in self.authorized_hosts: + offending.add(target) + else: + if target not in self.authorized_hosts and not any(address in network for network in networks): + offending.add(target) + + if offending: + rendered = ", ".join(sorted(offending)) + return ScopeDecision(False, f"Targets outside authorized scope: {rendered}", frozenset(offending)) + return ScopeDecision(True, "All effective targets are within the configured engagement scope") + + def to_opsec_context(self, targets: list[str]) -> dict[str, Any]: + """Adapt this scope to the context consumed by LegalComplianceChecker.""" + normalized_targets = sorted(normalized for target in targets if (normalized := normalize_target(target))) + return { + "authorized": self.is_configured(), + "scope": { + "targets": sorted(self.authorized_hosts), + "ip_ranges": list(self.authorized_ip_ranges), + }, + "targets": normalized_targets, + } + + +def _as_string_list(value: Any) -> list[str]: + if value is None: + return [] + if isinstance(value, str): + return [item.strip() for item in value.split(",") if item.strip()] + if isinstance(value, Iterable) and not isinstance(value, (bytes, bytearray, Mapping)): + return [str(item).strip() for item in value if str(item).strip()] + raise ValueError("Scope hosts and IP ranges must be strings or lists of strings") + + +def _optional_string(value: Any) -> str | None: + if value is None: + return None + rendered = str(value).strip() + return rendered or None diff --git a/pentestgpt/core/exceptions.py b/pentestgpt/core/exceptions.py index c3f417d..3233dfc 100644 --- a/pentestgpt/core/exceptions.py +++ b/pentestgpt/core/exceptions.py @@ -104,6 +104,23 @@ def __init__( self.required_permission = required_permission +class OutOfScopeError(AuthorizationError): + """Raised when an operation includes a target outside engagement scope.""" + + def __init__( + self, + message: str, + offending_targets: set[str] | None = None, + phase: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize an out-of-scope authorization failure.""" + super().__init__(message, required_permission="engagement_scope", **kwargs) + self.error_code = "OUT_OF_SCOPE" + self.offending_targets = offending_targets or set() + self.phase = phase + + class DatabaseError(PentestGPTError): """Database-related exceptions.""" diff --git a/pentestgpt/core/scope_guard.py b/pentestgpt/core/scope_guard.py new file mode 100644 index 0000000..3b10319 --- /dev/null +++ b/pentestgpt/core/scope_guard.py @@ -0,0 +1,71 @@ +"""Session-independent engagement scope enforcement and audit logging.""" + +from __future__ import annotations + +import json +from datetime import datetime, timezone +from pathlib import Path +from threading import Lock +from typing import Any, Protocol + +from pentestgpt.core.engagement import EngagementScope, ScopeDecision, normalize_target +from pentestgpt.core.exceptions import OutOfScopeError + + +class AuditSink(Protocol): + """Append-only destination for scope decisions.""" + + def write(self, entry: dict[str, Any]) -> None: + """Persist one audit entry.""" + ... + + +class JsonlAuditSink: + """Append scope decisions to a JSON Lines file.""" + + def __init__(self, path: str | Path) -> None: + self.path = Path(path) + self._lock = Lock() + + def write(self, entry: dict[str, Any]) -> None: + """Append one JSON object, creating the parent directory on demand.""" + line = json.dumps(entry, sort_keys=True, default=str) + with self._lock: + self.path.parent.mkdir(parents=True, exist_ok=True) + with self.path.open("a", encoding="utf-8") as audit_file: + audit_file.write(f"{line}\n") + + +class ScopeGuard: + """Validate scope independently of the optional OPSEC session.""" + + def __init__(self, scope: EngagementScope | None, audit_sink: AuditSink) -> None: + self.scope = scope + self.audit_sink = audit_sink + self.last_allowed_targets: frozenset[str] = frozenset() + + def check(self, phase: str, targets: set[str]) -> ScopeDecision: + """Audit and enforce a scope decision for one lifecycle phase.""" + normalized_targets = {normalized for target in targets if (normalized := normalize_target(target))} + if self.scope is None: + decision = ScopeDecision(True, "No engagement scope configured; authorization is not asserted") + else: + decision = self.scope.validate_targets(normalized_targets) + + entry = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "phase": phase, + "decision": "allow" if decision.allowed else "deny", + "scope_configured": self.scope is not None and self.scope.is_configured(), + "client": self.scope.client if self.scope else None, + "engagement_id": self.scope.engagement_id if self.scope else None, + "targets": sorted(normalized_targets), + "offending_targets": sorted(decision.offending), + "reason": decision.reason, + } + self.audit_sink.write(entry) + + if not decision.allowed: + raise OutOfScopeError(decision.reason, offending_targets=set(decision.offending), phase=phase) + self.last_allowed_targets = frozenset(normalized_targets) + return decision diff --git a/pentestgpt/detection_resistance/evasion_engine.py b/pentestgpt/detection_resistance/evasion_engine.py index 0f2fc97..3e783ef 100644 --- a/pentestgpt/detection_resistance/evasion_engine.py +++ b/pentestgpt/detection_resistance/evasion_engine.py @@ -22,6 +22,12 @@ from loguru import logger from pentestgpt.config.stealth_config import EvasionType, StealthConfig +from pentestgpt.prompts.authorization import ( + DETECTION_EVASION_ANALYST_ROLE, + EVASION_OPSEC_ROLE, + PAYLOAD_EVASION_ROLE, + build_system_prompt, +) from pentestgpt.utils.model_router import ModelRouter, TaskType @@ -685,7 +691,7 @@ def transform_payload( response, _provider, cost = self.router.route_task( TaskType.COMMAND_GENERATION, transformation_prompt, - system_prompt="You are a security expert specializing in payload transformation and evasion.", + system_prompt=build_system_prompt(PAYLOAD_EVASION_ROLE), ) result = json.loads(response) @@ -822,7 +828,7 @@ def optimize_for_environment( response, _provider, cost = self.router.route_task( TaskType.ANALYSIS, optimization_prompt, - system_prompt="You are an expert in evasion techniques and operational security.", + system_prompt=build_system_prompt(EVASION_OPSEC_ROLE), ) recommendations = json.loads(response) @@ -882,7 +888,7 @@ def validate_evasion_effectiveness( response, _provider, cost = self.router.route_task( TaskType.ANALYSIS, validation_prompt, - system_prompt="You are a security analyst specializing in detection and evasion assessment.", + system_prompt=build_system_prompt(DETECTION_EVASION_ANALYST_ROLE), ) assessment = json.loads(response) diff --git a/pentestgpt/effectiveness/success_rate_analytics.py b/pentestgpt/effectiveness/success_rate_analytics.py index b207467..36493de 100644 --- a/pentestgpt/effectiveness/success_rate_analytics.py +++ b/pentestgpt/effectiveness/success_rate_analytics.py @@ -17,6 +17,11 @@ from loguru import logger from pentestgpt.mitre import get_technique +from pentestgpt.prompts.authorization import ( + FALSE_POSITIVE_ANALYST_ROLE, + SUCCESS_FACTOR_ANALYST_ROLE, + build_system_prompt, +) from pentestgpt.utils.model_router import ModelRouter, TaskType @@ -920,7 +925,7 @@ def _ai_powered_false_positive_detection( response, _, _cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt="You are a cybersecurity expert specializing in false positive detection.", + system_prompt=build_system_prompt(FALSE_POSITIVE_ANALYST_ROLE), ) ai_results = json.loads(response) @@ -1377,7 +1382,7 @@ def _ai_powered_success_factor_analysis( response, _, _cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt="You are a data analyst specializing in success factor identification.", + system_prompt=build_system_prompt(SUCCESS_FACTOR_ANALYST_ROLE), ) ai_factors = json.loads(response) diff --git a/pentestgpt/effectiveness/validation_confirmation_system.py b/pentestgpt/effectiveness/validation_confirmation_system.py index 739c11a..b761490 100644 --- a/pentestgpt/effectiveness/validation_confirmation_system.py +++ b/pentestgpt/effectiveness/validation_confirmation_system.py @@ -16,6 +16,10 @@ from loguru import logger +from pentestgpt.prompts.authorization import ( + ATTACK_RESULT_VALIDATOR_ROLE, + build_system_prompt, +) from pentestgpt.utils.model_router import ModelRouter, TaskType @@ -1066,7 +1070,7 @@ def _validate_via_ai_powered_validation( response, _, cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt="You are a cybersecurity expert specializing in attack result validation.", + system_prompt=build_system_prompt(ATTACK_RESULT_VALIDATOR_ROLE), ) ai_validation = json.loads(response) diff --git a/pentestgpt/experimental/_mitre_prototype.py b/pentestgpt/experimental/_mitre_prototype.py index 392e5f1..c4c5b70 100644 --- a/pentestgpt/experimental/_mitre_prototype.py +++ b/pentestgpt/experimental/_mitre_prototype.py @@ -9,6 +9,7 @@ from loguru import logger from pentestgpt.llms.router import ModelRouter, TaskType +from pentestgpt.prompts.authorization import MITRE_EXPERT_ROLE, build_system_prompt class MITRETactic(Enum): @@ -315,7 +316,7 @@ def generate_tasks_for_tactic( response, _, _ = self.router.route_task( TaskType.STRATEGIC_PLANNING, prompt, - system_prompt="You are a penetration testing expert familiar with MITRE ATT&CK.", + system_prompt=build_system_prompt(MITRE_EXPERT_ROLE), ) # Parse response and create PTT nodes diff --git a/pentestgpt/llms/router.py b/pentestgpt/llms/router.py index 924f3e9..f76a94f 100644 --- a/pentestgpt/llms/router.py +++ b/pentestgpt/llms/router.py @@ -11,6 +11,7 @@ from __future__ import annotations +import hashlib import time from enum import Enum from typing import Any @@ -120,7 +121,7 @@ def __init__( self.task_history: list[dict[str, Any]] = [] # Caching - self._routing_cache: dict[tuple[TaskType, str], tuple[str, str, float]] = {} + self._routing_cache: dict[tuple[TaskType, str, str], tuple[str, str, float]] = {} self._max_cache_size = 1000 self._provider_health_cache: dict[str, dict[str, Any]] = {} self._last_health_check: float = 0.0 @@ -308,8 +309,9 @@ def route_task( """ start_time = time.time() - # Check cache (keyed on first 100 chars of message) - cache_key = (task_type, message[:100]) + # Isolate cached responses by both user message and system-prompt identity. + system_prompt_hash = hashlib.sha256((system_prompt or "").encode()).hexdigest()[:16] + cache_key = (task_type, message[:100], system_prompt_hash) if cache_key in self._routing_cache: self.performance_metrics["cache_hits"] += 1 logger.debug(f"Cache hit for {task_type.value} task") diff --git a/pentestgpt/monitoring/alert_correlation.py b/pentestgpt/monitoring/alert_correlation.py index 0379718..7075988 100644 --- a/pentestgpt/monitoring/alert_correlation.py +++ b/pentestgpt/monitoring/alert_correlation.py @@ -24,6 +24,10 @@ MonitoredEvent, MonitorSource, ) +from pentestgpt.prompts.authorization import ( + CORRELATION_ANALYST_ROLE, + build_system_prompt, +) from pentestgpt.utils.model_router import ModelRouter, TaskType from pentestgpt.utils.strategy_adaptation_controller import ( DetectionEvent, @@ -1144,7 +1148,7 @@ def _perform_ai_correlation_analysis(self, event: MonitoredEvent) -> None: response, _provider, cost = self.model_router.route_task( TaskType.SECURITY_ANALYSIS, correlation_prompt, - system_prompt="You are an expert in cybersecurity correlation analysis and attack pattern recognition.", + system_prompt=build_system_prompt(CORRELATION_ANALYST_ROLE), ) # Parse AI response diff --git a/pentestgpt/monitoring/detection_intelligence.py b/pentestgpt/monitoring/detection_intelligence.py index 9628fe6..c9896a9 100644 --- a/pentestgpt/monitoring/detection_intelligence.py +++ b/pentestgpt/monitoring/detection_intelligence.py @@ -27,6 +27,10 @@ MonitoredEvent, MonitorSource, ) +from pentestgpt.prompts.authorization import ( + EVASION_INTELLIGENCE_ROLE, + build_system_prompt, +) from pentestgpt.utils.model_router import ModelRouter, TaskType @@ -922,7 +926,7 @@ def _generate_adaptation_suggestions(self) -> None: response, _provider, cost = self.model_router.route_task( TaskType.STRATEGIC_PLANNING, suggestion_prompt, - system_prompt="You are an expert in cybersecurity evasion techniques and adaptive security systems.", + system_prompt=build_system_prompt(EVASION_INTELLIGENCE_ROLE), ) # Parse AI suggestions diff --git a/pentestgpt/monitoring/detection_monitor.py b/pentestgpt/monitoring/detection_monitor.py index 2a627d5..4753ee8 100644 --- a/pentestgpt/monitoring/detection_monitor.py +++ b/pentestgpt/monitoring/detection_monitor.py @@ -20,6 +20,10 @@ from pentestgpt.config.stealth_config import StealthConfig from pentestgpt.network.traffic_analyzer import NetworkTrafficAnalyzer +from pentestgpt.prompts.authorization import ( + THREAT_DETECTION_ANALYST_ROLE, + build_system_prompt, +) from pentestgpt.utils.model_router import ModelRouter, TaskType from pentestgpt.utils.strategy_adaptation_controller import ( DetectionEvent, @@ -694,7 +698,7 @@ def _perform_ai_analysis(self, event: MonitoredEvent) -> None: response, _provider, cost = self.model_router.route_task( TaskType.SECURITY_ANALYSIS, analysis_prompt, - system_prompt="You are an expert in cybersecurity threat detection and analysis.", + system_prompt=build_system_prompt(THREAT_DETECTION_ANALYST_ROLE), ) # Parse AI response diff --git a/pentestgpt/monitoring/security_event_analyzer.py b/pentestgpt/monitoring/security_event_analyzer.py index ec4f12c..b9c023c 100644 --- a/pentestgpt/monitoring/security_event_analyzer.py +++ b/pentestgpt/monitoring/security_event_analyzer.py @@ -28,6 +28,10 @@ MonitoredEvent, MonitorSource, ) +from pentestgpt.prompts.authorization import ( + SECURITY_LOG_ANALYST_ROLE, + build_system_prompt, +) from pentestgpt.utils.model_router import ModelRouter, TaskType @@ -1213,7 +1217,7 @@ def _perform_ai_behavioral_analysis(self, log_entries: list[LogEntry]) -> None: response, _provider, cost = self.model_router.route_task( TaskType.SECURITY_ANALYSIS, analysis_prompt, - system_prompt="You are an expert in log analysis and behavioral security monitoring.", + system_prompt=build_system_prompt(SECURITY_LOG_ANALYST_ROLE), ) # Parse AI response diff --git a/pentestgpt/orchestrator.py b/pentestgpt/orchestrator.py index f8bee19..9d24179 100644 --- a/pentestgpt/orchestrator.py +++ b/pentestgpt/orchestrator.py @@ -7,6 +7,7 @@ import time from collections.abc import Callable from dataclasses import dataclass +from pathlib import Path from typing import TYPE_CHECKING, Any, cast from uuid import uuid4 @@ -43,6 +44,8 @@ StealthOperationType, ) from pentestgpt.config.stealth_config import StealthConfig +from pentestgpt.core.engagement import EngagementScope, extract_effective_targets +from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard from pentestgpt.metrics_collector import MetricsCollector from pentestgpt.mitre import MITREGuidedPlanner, MITRETactic, get_technique from pentestgpt.report_generator import ReportGenerator @@ -102,8 +105,16 @@ def __init__( session_dir: str = "sessions", status_callback: Callable[[str], None] | None = None, subsystems: OptionalSubsystems | None = None, + engagement_scope: EngagementScope | None = None, + scope_guard: ScopeGuard | None = None, ): """Initialize PentestOrchestrator.""" + self.session_dir = session_dir + self.engagement_scope = engagement_scope + self.scope_guard = scope_guard or ScopeGuard( + engagement_scope, + JsonlAuditSink(Path(session_dir) / "scope_audit.jsonl"), + ) self.router = model_router self.strategy_agent = strategy_agent self.status_callback = status_callback @@ -129,10 +140,23 @@ def __init__( else: self.opsec_agent = opsec_agent + scope_aware_agents = ( + self.strategy_agent, + self.command_generator, + self.repetition_detector, + self.results_verifier, + self.executor, + self.stealth_agent, + self.opsec_agent, + self._subs.exploitation_agent, + ) + for agent in scope_aware_agents: + if agent is not None and hasattr(agent, "set_engagement_scope"): + agent.set_engagement_scope(self.engagement_scope, self.scope_guard) + self.session_id = str(uuid4()) self.max_iterations = max_iterations self.current_iteration = 0 - self.session_dir = session_dir self.target_info: dict[str, Any] | None = None # Initialize PTT Manager @@ -206,6 +230,7 @@ async def run(self, target_info: dict[str, Any]) -> dict[str, Any]: logger.info(f"Target: {target_info}") self.target_info = target_info + self.scope_guard.check("session_start", self._effective_targets(target_info)) start_time = time.time() # Load similar historical assessments now that target_info is available @@ -576,6 +601,7 @@ def _handle_repetition_check(self, next_task: PTTNode, target_info: dict[str, An async def _dispatch_executor(self, verified_commands: list[str]) -> dict[str, Any]: """Dispatch commands to the appropriate executor interface.""" + self.scope_guard.check("command_execution", self._effective_targets()) if hasattr(self.executor, "execute_batch_async") and asyncio.iscoroutinefunction( getattr(self.executor, "execute_batch_async", None) ): @@ -637,6 +663,7 @@ def _determine_task_status(self, task: PTTNode, findings: dict[str, Any], target async def _generate_commands(self, task: PTTNode, target_info: dict[str, Any]) -> list[str]: """Generate commands for a task using Command Generator agent with caching.""" logger.info(f"Generating commands for task: {task.name}") + self.scope_guard.check("command_generation", self._effective_targets(target_info)) start_time = time.time() # Use cached technique lookup @@ -720,7 +747,7 @@ def _execute_commands_sync(self, commands: list[str]) -> dict[str, Any]: results_list = [] for cmd in commands: # Step 1: Verification - if self.opsec_agent: + if self.opsec_agent and self.opsec_agent.current_session: is_valid, reason = self.opsec_agent.validate_operation( operation="command_execution", context={"command": cmd, "target_info": self.target_info}, @@ -758,6 +785,13 @@ def _execute_commands_sync(self, commands: list[str]) -> dict[str, Any]: return {"executed": True, "outputs": results_list} + def _effective_targets(self, target_info: dict[str, Any] | None = None) -> set[str]: + """Return the primary and discovered hosts effective for the next call.""" + return extract_effective_targets( + target_info or self.target_info or {}, + {"discovered_hosts": self._session_findings.get("discovered_hosts", [])}, + ) + def _get_alternative_approach( self, task: PTTNode, diff --git a/pentestgpt/prompts/authorization.py b/pentestgpt/prompts/authorization.py new file mode 100644 index 0000000..6298cbd --- /dev/null +++ b/pentestgpt/prompts/authorization.py @@ -0,0 +1,80 @@ +"""Authorization-aware system prompt construction.""" + +from __future__ import annotations + +from collections.abc import Iterable + +from pentestgpt.core.engagement import EngagementScope, normalize_target + +MITRE_PENTEST_ROLE = """You are an expert penetration tester with deep knowledge of the MITRE ATT&CK framework. +Your responses should: +1. Align with MITRE ATT&CK tactics and techniques +2. Provide specific, executable commands +3. Consider detection and evasion strategies +4. Follow ethical penetration testing practices +5. Cite relevant MITRE technique IDs when applicable + +Always prioritize stealth and operational security while maintaining effectiveness.""" + +SENIOR_REPORT_WRITER_ROLE = "You are a senior penetration tester writing a professional report." +MITRE_EXPERT_ROLE = "You are a penetration testing expert familiar with MITRE ATT&CK." +PAYLOAD_EVASION_ROLE = "You are a security expert specializing in payload transformation and evasion." +EVASION_OPSEC_ROLE = "You are an expert in evasion techniques and operational security." +DETECTION_EVASION_ANALYST_ROLE = "You are a security analyst specializing in detection and evasion assessment." +OPSEC_RISK_ASSESSMENT_ROLE = "You are a security expert specializing in operational security and risk assessment." +OPSEC_PENTEST_ROLE = "You are an expert in operational security and penetration testing OPSEC." +SECURITY_OUTPUT_PARSER_ROLE = "You are an expert at parsing security tool outputs." +ADAPTIVE_SECURITY_STRATEGIST_ROLE = "You are an expert in adaptive security and penetration testing strategy." +FALSE_POSITIVE_ANALYST_ROLE = "You are a cybersecurity expert specializing in false positive detection." +SUCCESS_FACTOR_ANALYST_ROLE = "You are a data analyst specializing in success factor identification." +ATTACK_RESULT_VALIDATOR_ROLE = "You are a cybersecurity expert specializing in attack result validation." +THREAT_DETECTION_ANALYST_ROLE = "You are an expert in cybersecurity threat detection and analysis." +SECURITY_LOG_ANALYST_ROLE = "You are an expert in log analysis and behavioral security monitoring." +EVASION_INTELLIGENCE_ROLE = "You are an expert in cybersecurity evasion techniques and adaptive security systems." +CORRELATION_ANALYST_ROLE = "You are an expert in cybersecurity correlation analysis and attack pattern recognition." +EXPLOIT_DEVELOPER_ROLE = "You are an expert in penetration testing and exploit development." + + +def build_authorization_preamble( + scope: EngagementScope | None, + effective_targets: Iterable[str] = frozenset(), +) -> str: + """Render neutral, authorized, or refusal framing for a target set.""" + targets = {normalized for target in effective_targets if (normalized := normalize_target(str(target)))} + if scope is None or not scope.is_configured(): + return ( + "ENGAGEMENT CONTEXT\n" + "No engagement scope is configured. Treat target permission as unverified, do not claim that the " + "activity is authorized, and limit the response to the stated request." + ) + + decision = scope.validate_targets(targets) + if not decision.allowed: + return ( + "ENGAGEMENT SCOPE VIOLATION\n" + f"{decision.reason}. Do not provide operational guidance for this request; explain that the targets " + "must be validated against the configured scope first." + ) + + identity_parts = [] + if scope.client: + identity_parts.append(f"client={scope.client}") + if scope.engagement_id: + identity_parts.append(f"engagement_id={scope.engagement_id}") + identity = f" ({', '.join(identity_parts)})" if identity_parts else "" + target_list = ", ".join(sorted(targets)) + roe = f" Rules of engagement: {scope.roe_notes}" if scope.roe_notes else "" + return ( + "AUTHORIZED ENGAGEMENT CONTEXT\n" + f"Scope validation passed for this sanctioned penetration-testing engagement{identity}. " + f"The effective targets for this request are: {target_list}.{roe} Stay strictly within these validated targets." + ) + + +def build_system_prompt( + role: str, + scope: EngagementScope | None = None, + effective_targets: Iterable[str] = frozenset(), +) -> str: + """Prepend authorization framing to an LLM role prompt.""" + return f"{build_authorization_preamble(scope, effective_targets)}\n\n{role.strip()}" diff --git a/pentestgpt/prompts/mitre_prompts.py b/pentestgpt/prompts/mitre_prompts.py index 0015e52..06f3131 100644 --- a/pentestgpt/prompts/mitre_prompts.py +++ b/pentestgpt/prompts/mitre_prompts.py @@ -9,24 +9,21 @@ from typing import Any +from pentestgpt.core.engagement import EngagementScope from pentestgpt.mitre import MITRETactic, MITRETechnique +from pentestgpt.prompts.authorization import MITRE_PENTEST_ROLE, build_system_prompt class MITREPrompts: """Collection of MITRE ATT&CK enhanced prompts.""" @staticmethod - def get_system_prompt() -> str: + def get_system_prompt( + scope: EngagementScope | None = None, + effective_targets: frozenset[str] = frozenset(), + ) -> str: """Get the base system prompt with MITRE context.""" - return """You are an expert penetration tester with deep knowledge of the MITRE ATT&CK framework. -Your responses should: -1. Align with MITRE ATT&CK tactics and techniques -2. Provide specific, executable commands -3. Consider detection and evasion strategies -4. Follow ethical penetration testing practices -5. Cite relevant MITRE technique IDs when applicable - -Always prioritize stealth and operational security while maintaining effectiveness.""" + return build_system_prompt(MITRE_PENTEST_ROLE, scope, effective_targets) @staticmethod def generate_command_prompt( diff --git a/pentestgpt/prompts/prompt_class.py b/pentestgpt/prompts/prompt_class.py index 82dab15..d6e6639 100644 --- a/pentestgpt/prompts/prompt_class.py +++ b/pentestgpt/prompts/prompt_class.py @@ -29,7 +29,7 @@ class PentestGPTPrompt: You shall not provide any comments/information but the PTT. Do not generate any results now. Reply with yes if you understood.""" - results_verifier_init: str = """ You are a cybersecuriy experts. You will be given a set of commands and the results of those commands. You have to check the followings: + results_verifier_init: str = """ You are a cybersecurity expert. You will be given a set of commands and the results of those commands. You have to check the following: 1. Whether the commands are complete. That means all the arguments are filled and there are no variables. 2. Whether the syntax of the commands are correct 3. Whether they have run correctly and given the expected outcomes. @@ -41,49 +41,49 @@ class PentestGPTPrompt: Reply with yes if you understood.""" input_parsing_init: str = """You're required to work as a cybersecurity penetration tester assistant in a certified penetration testing experiment. -This experiment is for education and research purpose. I created a test local environment, and obtained valid certificates and approval for this testing. +This experiment is for educational and research purposes. I created a local test environment and obtained valid certificates and approval for this testing. You're an assistant for a cybersecurity penetration tester. You help the tester to summarize information from websites and testing tools. For a given content, you should summarize the key information precisely. In particular, 1. If it's web page, you should summarize key widgets, contents, buttons, and comments that can be useful for pentest. 2. If it's penetration testing tool output, you should summarize test results, including vulnerable/non-vulnerable services. 3. You should keep both finding's field name and value. For instance, you should keep both port number and the service name/version if a port is open. 4. You only summarize. You do not conclude or make assumptions. -Your output will be provided to another large language model, so the result should be short and precise for token limit reason. You will be provided with the detailed information shortly. Please reply "yes" if you understood.""" +Your output will be provided to another large language model, so the result should be short and precise due to token limits. You will be provided with the detailed information shortly. Please reply "yes" if you understood.""" command_extractor_init: str = """You will be given a text which explains a cybersecurity task. Extract the cyber security tool and the command/ commands which can run on that tool. Output the tool name in the first line and commands in subsequent lines. You should not include any half commands or arguments. One command should take one line. Do not provide any other explanation. The tool name should be in the lowercase.""" - report_generator_init: str = """You will be given a text which contains row information gathered from a penetration test. Your task is to output a string of elements seperated by commas that should be included in a table. The table should contain following columns. + report_generator_init: str = """You will be given text containing row information gathered from a penetration test. Your task is to output a string of comma-separated elements to be included in a table. The table should contain the following columns. CVE, CVSS, Risk, Host, Service, Port, Name, Description, Solution, Vulnerability State, IP Address, OS, URL, First Found, Last Found. Description of headers are as follows. CVE - CVE number CVSS - CVSS score - Risk - whether it is High, Medium or Low rist + Risk - whether it is High, Medium or Low risk Host - The IP or the name of the Host - Sevice - The service name that is tested + Service - The service name that is tested Description - a short description about the vulnerability - Solution - a possible remidiation method - Vulnerability state: Whether it present or not in the machine + Solution - a possible remediation method + Vulnerability state: Whether it is present on the machine IP address - IP address of the host machine OS - OS of the machine - URL - a reference URL for the vulerability + URL - a reference URL for the vulnerability First found - the first found date of the vulnerability Last found - the last found date of the vulnerability - Analyse the given text and generate the string of elements in order, seperated by commas, which matches for each of the above heading. If you cannot find a suitable value for a heading, put NA there. The output must only contain the string containing 13 elements seperated by commas.""" + Analyze the given text and generate the string of elements in order, separated by commas, matching each of the above headings. If you cannot find a suitable value for a heading, put NA there. The output must only contain the string containing 15 elements separated by commas.""" report_generate: str = ( - "Analyze the followign text and output a string of element seperated by commas to update the above table. If there is no information for a particular column, put 'NA' for that element. Make sure the number of columns matches with the example above. Output must only contain the string of element.\n" + "Analyze the following text and output a string of comma-separated elements to update the above table. If there is no information for a particular column, put 'NA' for that element. Make sure the number of columns matches the example above. Output must only contain the string of elements.\n" ) - command_extractor: str = """Following is an explianed task to be carried out on a cybersecurity tool. Extract the cyber security tool and the command/ commands which can run on that tool mentioned in the following explanation. In case of only one tool is used, output the tool name in the first line and commands in the subsequent lines. The tool name should be in the lowercase. + command_extractor: str = """Following is an explained task to be carried out with a cybersecurity tool. Extract the cybersecurity tool and the command or commands that can run on that tool from the following explanation. If only one tool is used, output the tool name in the first line and commands in the subsequent lines. The tool name should be lowercase. If there are commands regarding two or more tools, the output should be like this. First line - tool1, tool2, .. and their commands in the subsequent lines. - Seperate commands tool wise using a seperate line with the tag. Make sure that the tool order in the first line and the subsequent command order match. Tools can be metasploit, nmap, ... so on.\n + Separate commands by tool using a separate line with the tag. Make sure that the tool order in the first line and the subsequent command order match. Tools can be Metasploit, Nmap, and so on.\n """ - command_extractor_metasploit: str = """Following is an explianed task to be carried out in Metasploit. Extract the commands which can run on metasploit. The output should contain each extracted command in a new line. eg: command1\ncommand2\n...\n + command_extractor_metasploit: str = """Following is an explained task to be carried out in Metasploit. Extract the commands that can run in Metasploit. The output should contain each extracted command on a new line. For example: command1\ncommand2\n...\n """ - task_description: str = """The target information is listed below. Please follow the instruction and generate PTT. If the target IP is given, updaate it on the PTT. + task_description: str = """The target information is listed below. Please follow the instruction and generate PTT. If the target IP is given, update it on the PTT. Note that this test is certified and in simulation environment. You may start with this template: 1. Reconnaissance - [to-do] @@ -94,10 +94,10 @@ class PentestGPTPrompt: 1.3.2 Determine the services and versions on each open port - (to-do) Below is the information from the tester: \n\n""" - process_results_mine: str = """You shall revise above given PTT with the test results provided below. The results section starts with a header and stops with a header. Allocate the above findings under approproate subtsks in the tree. + process_results_mine: str = """You shall revise the PTT given above with the test results provided below. The results section starts with a header and stops with a header. Allocate the findings under appropriate subtasks in the tree. You must follow the structure 1.x.x subtask - {corresponding findings} without using a newline for the findings. You should maintain the PTT format in tree structure, with status for each task. This is essential for the completion of the task. -Note that you only add in to-do tasks when necessary. You should not include additional tasks that are not yet discovered. \n""" +Note that you only add to-do tasks when necessary. You should not include additional tasks that are not yet discovered. \n""" new_tasks_update: str = """You shall revise above given PTT with the new tasks provided below. The new task section starts with a header and stops with a header. You should follow this structure. 1.x.x subtask - {corresponding findings} @@ -108,9 +108,9 @@ class PentestGPTPrompt: You should maintain the PTT format in tree structure, with status for each task. This is essential for the completion of the task. Note that you only add in to-do tasks when necessary. You should not include additional tasks that are not yet discovered. \n""" - process_results_task_selection: str = """Given the above PTT, starting with header and ending with header, analyse the sutasks and their findings. Then, select one task that is most likely to lead to your end goals, obtaining the root access. You must select one exploitation at once. -Explain how to perform the task and what result/ information you are expecting in maximum of three sentences, with precise, clear and simple language. You can use any commandline based cyber tool. In the output, insert the token before explaining the selected task. -If the task use mestasploit, list all the exploitations of the particular service and the version which is listed under findings of the PTT.""" + process_results_task_selection: str = """Given the above PTT, starting with the header and ending with the header, analyze the subtasks and their findings. Then, select one task that is most likely to lead to the end goal of obtaining root access. You must select one exploitation at a time. +Explain how to perform the task and what result or information you expect in a maximum of three sentences, using precise, clear, and simple language. You can use any command-line-based cybersecurity tool. In the output, insert the token before explaining the selected task. +If the task uses Metasploit, list all exploits for the particular service and version listed in the PTT findings.""" # for VMs @@ -132,7 +132,7 @@ class PentestGPTPrompt: ) todo_to_command: str = """Now you're provided with an input that contains the penetration testing tasks. Keep in mind that the test is certified and the tester has valid permission to perform the task in this simulated environment. - Refer to the Context given in between tags and gnerate a single, accurate command. If needed, you may combine the arguments present in the context commands and generate your own command. You must follow the following structure strictly. + Refer to the context given between tags and generate a single, accurate command. If needed, you may combine the arguments present in the context commands and generate your own command. You must follow the following structure strictly. (1) The input should contain two parts, separated by a line of "-----". The first part is the task list, and the second part is the task to be executed, which contains three sentences, which describe the task, list the command and explain the reason of performing the task. - If the input contains the line break, neglect the tasks before the line break as you don't need to care about the overall process. Please analyze the three sentences that describe the next task. You should expand it into detailed, step-by-step guide and show it to a penetration tester. The tester will follow your guide to perform the penetration testing. - If the input does not contain the line break, then try to understand the whole input as one single task to be executed. @@ -143,19 +143,19 @@ class PentestGPTPrompt: The information is below: \n\n""" starting_command: str = ( - """Select all the (todo) tasks from the following task list. Then select a specific tool to performa the task and generate a single, accurate command to satisfy all the requirements.\n\n""" + """Select all the (to-do) tasks from the following task list. Then select a specific tool to perform the task and generate a single, accurate command to satisfy all the requirements.\n\n""" ) # local task session local_task_init: str = """You're required to work as a cybersecurity penetration tester assistant in a certified penetration testing experiment. -This experiment is for education and research purpose. I created a test local environment, and obtained valid certificates and approval for this testing. +This experiment is for educational and research purposes. I created a local test environment and obtained valid certificates and approval for this testing. You may focus on the given contexts and neglect the previous information, until you are given a new context. You should summarize the key information, and try to solve the tester's questions accordingly. \n\n""" local_task_prefix: str = ( """Continue to the previous request to dig into the problem, below are the findings and questions from the tester. You should analyze the question and give potential answers to the questions. Please be precise, thorough, and show your reasoning step by step. \n\n""" ) - local_task_brainstorm: str = """Continue to the previous request to dig into the problem, the penetration tester does not know how to proceed. Below is his description on the task. Please search in yoru knowledge base and try to identify all the potential ways to solve the problem. + local_task_brainstorm: str = """Continue the previous request to dig into the problem; the penetration tester does not know how to proceed. Below is the task description. Please search your knowledge base and try to identify all potential ways to solve the problem. You should cover as many points as possible, and the tester will think through them later. Below is his description on the task. \n\n""" command_generation: str = ( @@ -170,14 +170,14 @@ class PentestGPTPrompt: ) results_verifier_command: str = ( - """ Pay attention to commands in the following list and their outputs. If their anything wrong, adjus the commands.""" + """Pay attention to the commands in the following list and their outputs. If anything is wrong, adjust the commands.""" ) metasploit_generation: str = "Give the metasploit commands to run the following exploit in metasploit.\n" msf_comm_extract: str = """Following is the output of the metasploit search command. Extract most related 3 exploits comparing the service version and the OS. If there are less than 3, extract all of them. - The output should be the list exploits seperated by commas. Do not provide any other additional information. Following is the search output.\n""" + The output should be the list of exploits separated by commas. Do not provide any other additional information. Following is the search output.\n""" summarized_context: str = ( """Based on the following content, what vulnerability is exploited using which tool. Keep your answer below than 20 words\n""" diff --git a/pentestgpt/report_generator.py b/pentestgpt/report_generator.py index 9a749c0..27dc868 100644 --- a/pentestgpt/report_generator.py +++ b/pentestgpt/report_generator.py @@ -8,6 +8,10 @@ from loguru import logger from pentestgpt.mitre import MITREGuidedPlanner +from pentestgpt.prompts.authorization import ( + SENIOR_REPORT_WRITER_ROLE, + build_system_prompt, +) from pentestgpt.prompts.mitre_prompts import MITREPrompts from pentestgpt.utils.model_router import ModelRouter, TaskType from pentestgpt.utils.ptt_manager import PTTManager @@ -61,7 +65,7 @@ async def generate( response, _, _ = router.route_task( TaskType.REPORT_GENERATION, prompt, - system_prompt="You are a senior penetration tester writing a professional report.", + system_prompt=build_system_prompt(SENIOR_REPORT_WRITER_ROLE), ) try: diff --git a/pentestgpt/utils/dependency_injection.py b/pentestgpt/utils/dependency_injection.py index ba2d1cf..78601ec 100644 --- a/pentestgpt/utils/dependency_injection.py +++ b/pentestgpt/utils/dependency_injection.py @@ -11,11 +11,17 @@ - Environment-based configuration """ +from __future__ import annotations + import inspect -from typing import Any, Protocol, TypeVar, cast, runtime_checkable +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast, runtime_checkable from loguru import logger +if TYPE_CHECKING: + from pentestgpt.core.engagement import EngagementScope + from pentestgpt.core.scope_guard import ScopeGuard + # Type variables for generic dependency injection T = TypeVar("T") DependencyType = TypeVar("DependencyType") @@ -402,6 +408,19 @@ def __init__(self, container: DependencyContainer): container: Dependency container for resolving dependencies """ self.container = container + self._engagement_scope: EngagementScope | None = None + self._scope_guard: ScopeGuard | None = None + + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Configure scope state automatically injected into created agents.""" + self._engagement_scope = scope + self._scope_guard = guard + + def wire_engagement_scope(self, agent: T) -> T: + """Inject configured scope into a compatible agent and return it.""" + if self._scope_guard is not None and hasattr(agent, "set_engagement_scope"): + agent.set_engagement_scope(self._engagement_scope, self._scope_guard) + return agent def create_agent(self, agent_class: type[T], **kwargs: Any) -> T: """Create an agent instance. @@ -413,7 +432,8 @@ def create_agent(self, agent_class: type[T], **kwargs: Any) -> T: Returns: Initialized agent instance """ - return self.container.create_agent(agent_class, **kwargs) + agent = self.container.create_agent(agent_class, **kwargs) + return self.wire_engagement_scope(agent) def create_strategy_agent(self, **kwargs: Any) -> Any: """Create a StrategyAgent instance.""" diff --git a/pentestgpt/utils/orchestrator_factory.py b/pentestgpt/utils/orchestrator_factory.py index d99bf23..12fe94e 100644 --- a/pentestgpt/utils/orchestrator_factory.py +++ b/pentestgpt/utils/orchestrator_factory.py @@ -2,10 +2,13 @@ from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING, Any, cast from loguru import logger +from pentestgpt.core.engagement import EngagementScope +from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard from pentestgpt.mitre import MITREPlannerInterface from pentestgpt.orchestrator import OptionalSubsystems, PentestOrchestrator from pentestgpt.utils.dependency_injection import AgentFactory, DependencyContainer @@ -58,6 +61,7 @@ def create_orchestrator( Initialized PentestOrchestrator instance """ logger.info("Creating PentestOrchestrator with dependency injection") + engagement_scope, scope_guard = self._configure_engagement_scope(session_dir, kwargs) # Register core dependencies self.container.register_instance(type(model_router), model_router) @@ -137,6 +141,8 @@ def create_orchestrator( max_iterations=max_iterations, session_dir=session_dir, subsystems=subs, + engagement_scope=engagement_scope, + scope_guard=scope_guard, ) except Exception: if advanced_agent_manager is not None: @@ -200,6 +206,7 @@ def _create_exploitation_agent(self, model_router: Any, tool_executor: Any | Non ) agent = AdvancedExploitationAgent(model_router, tool_executor=tool_executor) + self.agent_factory.wire_engagement_scope(agent) logger.info("Advanced exploitation agent initialized") return agent except Exception as e: @@ -438,6 +445,7 @@ def create_orchestrator_from_container( logger.info("Creating PentestOrchestrator from dependency container") try: + engagement_scope, scope_guard = self._configure_engagement_scope(session_dir, kwargs) # Use provided instances directly; only resolve from container when not in kwargs model_router = kwargs.get("model_router") if model_router is None: @@ -549,6 +557,8 @@ def create_orchestrator_from_container( max_iterations=max_iterations, session_dir=session_dir, subsystems=subs, + engagement_scope=engagement_scope, + scope_guard=scope_guard, ) except Exception: if advanced_agent_manager is not None: @@ -590,6 +600,7 @@ def create_orchestrator_with_defaults( Initialized PentestOrchestrator instance with mock dependencies """ logger.info("Creating PentestOrchestrator with default mock dependencies") + engagement_scope, scope_guard = self._configure_engagement_scope(session_dir, kwargs) # Create mock dependencies from unittest.mock import MagicMock @@ -647,6 +658,8 @@ def create_orchestrator_with_defaults( tool_executor=None, # type: ignore max_iterations=max_iterations, session_dir=session_dir, + engagement_scope=engagement_scope, + scope_guard=scope_guard, ) # Manually set the ptt_manager orchestrator.ptt_manager = ptt_manager @@ -656,6 +669,25 @@ def create_orchestrator_with_defaults( ) return orchestrator + def _configure_engagement_scope( + self, + session_dir: str, + kwargs: dict[str, Any], + ) -> tuple[EngagementScope | None, ScopeGuard]: + """Build one scope/guard pair and configure the shared agent factory.""" + from pentestgpt.config.settings import app_settings + + scope = cast( + "EngagementScope | None", + kwargs.pop("engagement_scope", EngagementScope.from_settings(app_settings)), + ) + guard = cast( + "ScopeGuard | None", + kwargs.pop("scope_guard", None), + ) or ScopeGuard(scope, JsonlAuditSink(Path(session_dir) / "scope_audit.jsonl")) + self.agent_factory.set_engagement_scope(scope, guard) + return scope, guard + def create_orchestrator_factory() -> OrchestratorFactory: """Create a default orchestrator factory. diff --git a/pentestgpt/utils/strategy_adaptation_controller.py b/pentestgpt/utils/strategy_adaptation_controller.py index f6fda0d..1c7e5e4 100644 --- a/pentestgpt/utils/strategy_adaptation_controller.py +++ b/pentestgpt/utils/strategy_adaptation_controller.py @@ -21,6 +21,10 @@ from loguru import logger from pentestgpt.config.stealth_config import StealthConfig, StealthLevel +from pentestgpt.prompts.authorization import ( + ADAPTIVE_SECURITY_STRATEGIST_ROLE, + build_system_prompt, +) from pentestgpt.utils.model_router import ModelRouter, TaskType @@ -378,7 +382,7 @@ def _generate_ai_adaptations(self, event: DetectionEvent) -> list[AdaptationActi response, _provider, cost = self.router.route_task( TaskType.STRATEGIC_PLANNING, adaptation_prompt, - system_prompt="You are an expert in adaptive security and penetration testing strategy.", + system_prompt=build_system_prompt(ADAPTIVE_SECURITY_STRATEGIST_ROLE), ) ai_actions = json.loads(response) diff --git a/tests/agents/test_exploit_scope.py b/tests/agents/test_exploit_scope.py new file mode 100644 index 0000000..4910183 --- /dev/null +++ b/tests/agents/test_exploit_scope.py @@ -0,0 +1,72 @@ +"""Tests for scope enforcement in the exploitation sub-chain.""" + +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from pentestgpt.agents.advanced_exploitation_agent import AdvancedExploitationAgent +from pentestgpt.agents.exploit_developer import ExploitDeveloper, ExploitType +from pentestgpt.core.engagement import EngagementScope +from pentestgpt.core.exceptions import OutOfScopeError +from pentestgpt.core.scope_guard import ScopeGuard + + +class MemoryAuditSink: + def __init__(self) -> None: + self.entries: list[dict[str, Any]] = [] + + def write(self, entry: dict[str, Any]) -> None: + self.entries.append(entry) + + +def test_exploit_generation_uses_authorized_preamble() -> None: + router = MagicMock() + router.route_task.return_value = ("payload", "mock", 0.0) + developer = ExploitDeveloper({}, {}, model_router=router) + guard = ScopeGuard( + EngagementScope(authorized_hosts=frozenset({"lab.example.test"})), + MemoryAuditSink(), + ) + developer.set_engagement_scope(guard.scope, guard) + + payload = developer._generate_exploit_code( + ExploitType.XSS, + {"name": "test"}, + "lab.example.test", + ) + + assert payload == "payload" + assert "AUTHORIZED ENGAGEMENT CONTEXT" in router.route_task.call_args.kwargs["system_prompt"] + + +def test_out_of_scope_exploit_generation_skips_router() -> None: + router = MagicMock() + developer = ExploitDeveloper({}, {}, model_router=router) + guard = ScopeGuard( + EngagementScope(authorized_hosts=frozenset({"lab.example.test"})), + MemoryAuditSink(), + ) + developer.set_engagement_scope(guard.scope, guard) + + with pytest.raises(OutOfScopeError): + developer._generate_exploit_code( + ExploitType.XSS, + {"name": "test"}, + "outside.example.test", + ) + + router.route_task.assert_not_called() + + +def test_advanced_agent_forwards_scope_to_exploit_developer() -> None: + agent = AdvancedExploitationAgent(MagicMock()) + guard = ScopeGuard( + EngagementScope(authorized_hosts=frozenset({"lab.example.test"})), + MemoryAuditSink(), + ) + + agent.set_engagement_scope(guard.scope, guard) + + assert agent.exploit_developer._engagement_scope is guard.scope + assert agent.exploit_developer._scope_guard is guard diff --git a/tests/agents/test_opsec_agent.py b/tests/agents/test_opsec_agent.py index 39f956f..ad6cc4b 100644 --- a/tests/agents/test_opsec_agent.py +++ b/tests/agents/test_opsec_agent.py @@ -92,6 +92,20 @@ def test_compliant_operation_with_authorization(self) -> None: ) assert is_compliant is True + def test_compliant_operation_accepts_target_within_ip_range(self) -> None: + checker = LegalComplianceChecker() + + is_compliant, _reason = checker.validate_operation( + "port_scan", + { + "authorized": True, + "scope": {"targets": ["lab.example.test"], "ip_ranges": ["10.0.0.0/24"]}, + "targets": ["10.0.0.8", "lab.example.test"], + }, + ) + + assert is_compliant is True + def test_noncompliant_without_authorization(self) -> None: checker = LegalComplianceChecker() is_compliant, reason = checker.validate_operation( @@ -402,8 +416,8 @@ def test_generate_compliance_report_with_session(self) -> None: def test_validate_operation_no_session(self) -> None: agent = _make_opsec_agent() is_ok, reason = agent.validate_operation("scan", {"authorized": True, "scope": {}}) - assert is_ok is True - assert "No session" in reason + assert is_ok is False + assert "does not establish authorization" in reason def test_log_activity_with_no_session_does_not_raise(self) -> None: agent = _make_opsec_agent() diff --git a/tests/api/test_opsec_session_scope.py b/tests/api/test_opsec_session_scope.py new file mode 100644 index 0000000..1f3e1ef --- /dev/null +++ b/tests/api/test_opsec_session_scope.py @@ -0,0 +1,36 @@ +"""Tests for canonical scope propagation through the OPSEC API boundary.""" + +from unittest.mock import MagicMock + +from fastapi.testclient import TestClient + +from pentestgpt.api import stealth_api +from pentestgpt.core.engagement import EngagementScope + + +def test_authorized_scope_reaches_opsec_session(monkeypatch) -> None: + opsec = MagicMock() + opsec.create_session.return_value = "scope-session" + monkeypatch.setattr(stealth_api, "opsec_agent", opsec) + client = TestClient(stealth_api.app) + + response = client.post( + "/stealth/sessions", + json={ + "name": "Scoped test", + "authorized_scope": { + "client": "Example", + "engagement_id": "PT-10", + "hosts": ["Lab.Example.Test"], + "ip_ranges": ["10.40.0.0/16"], + "roe_notes": "No availability testing", + }, + }, + ) + + assert response.status_code == 200 + scope = opsec.create_session.call_args.kwargs["scope"] + assert isinstance(scope, EngagementScope) + assert scope.authorized_hosts == frozenset({"lab.example.test"}) + assert scope.authorized_ip_ranges == ("10.40.0.0/16",) + assert scope.engagement_id == "PT-10" diff --git a/tests/core/test_engagement.py b/tests/core/test_engagement.py new file mode 100644 index 0000000..f311c23 --- /dev/null +++ b/tests/core/test_engagement.py @@ -0,0 +1,98 @@ +"""Tests for canonical engagement scope normalization and validation.""" + +from pentestgpt.config.settings import AppSettings +from pentestgpt.core.engagement import ( + EngagementScope, + extract_effective_targets, + normalize_target, +) + + +def test_normalize_url_hostname_ip_and_ports() -> None: + assert normalize_target("HTTPS://Example.TEST:8443/path") == "example.test" + assert normalize_target("EXAMPLE.test:443") == "example.test" + assert normalize_target("[2001:db8::1]:443") == "2001:db8::1" + assert normalize_target("192.0.2.10:22") == "192.0.2.10" + + +def test_multi_target_validation_accepts_hosts_and_cidr_members() -> None: + scope = EngagementScope( + client="Example", + engagement_id="PT-1", + authorized_hosts=frozenset({"App.Example.Test"}), + authorized_ip_ranges=("10.10.0.0/16",), + ) + + decision = scope.validate_targets({"https://app.example.test/login", "10.10.4.8"}) + + assert decision.allowed is True + assert decision.offending == frozenset() + + +def test_configured_scope_fails_closed_for_empty_and_mixed_targets() -> None: + scope = EngagementScope( + authorized_hosts=frozenset({"app.example.test"}), + authorized_ip_ranges=("10.10.0.0/16",), + ) + + empty = scope.validate_targets(set()) + mixed = scope.validate_targets({"app.example.test", "192.0.2.50"}) + + assert empty.allowed is False + assert mixed.allowed is False + assert mixed.offending == frozenset({"192.0.2.50"}) + + +def test_unconfigured_scope_is_neutral_and_non_authorizing() -> None: + scope = EngagementScope() + + decision = scope.validate_targets(set()) + + assert decision.allowed is True + assert "not asserted" in decision.reason + + +def test_scope_from_settings_and_api_share_the_same_shape() -> None: + settings = AppSettings( + engagement_client="Example", + engagement_id="PT-2", + authorized_hosts=["Lab.Example.Test"], + authorized_ip_ranges=["10.20.0.1/16"], + rules_of_engagement="Business hours only", + ) + from_settings = EngagementScope.from_settings(settings) + from_api = EngagementScope.from_authorized_scope( + { + "client": "Example", + "engagement_id": "PT-2", + "targets": ["lab.example.test"], + "networks": ["10.20.0.0/16"], + "rules_of_engagement": "Business hours only", + } + ) + + assert from_settings == from_api + + +def test_extract_effective_targets_includes_primary_and_discovered_hosts() -> None: + targets = extract_effective_targets( + {"target": "https://APP.example.test:443/path"}, + {"session_findings": {"discovered_hosts": ["10.0.0.8", "Db.Example.Test"]}}, + ) + + assert targets == {"app.example.test", "10.0.0.8", "db.example.test"} + + +def test_to_opsec_context_uses_normalized_canonical_scope() -> None: + scope = EngagementScope( + authorized_hosts=frozenset({"App.Example.Test"}), + authorized_ip_ranges=("10.0.0.1/8",), + ) + + context = scope.to_opsec_context(["HTTPS://APP.EXAMPLE.TEST/path", "10.1.2.3"]) + + assert context == { + "authorized": True, + "scope": {"targets": ["app.example.test"], "ip_ranges": ["10.0.0.0/8"]}, + "targets": ["10.1.2.3", "app.example.test"], + } diff --git a/tests/core/test_scope_guard.py b/tests/core/test_scope_guard.py new file mode 100644 index 0000000..6c99cc4 --- /dev/null +++ b/tests/core/test_scope_guard.py @@ -0,0 +1,65 @@ +"""Tests for session-independent scope enforcement and auditing.""" + +import json +from pathlib import Path +from typing import Any + +import pytest + +from pentestgpt.core.engagement import EngagementScope +from pentestgpt.core.exceptions import OutOfScopeError +from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard + + +class MemoryAuditSink: + """Collect audit records without filesystem side effects.""" + + def __init__(self) -> None: + self.entries: list[dict[str, Any]] = [] + + def write(self, entry: dict[str, Any]) -> None: + self.entries.append(entry) + + +def test_guard_audits_allow_without_opsec_session() -> None: + sink = MemoryAuditSink() + guard = ScopeGuard( + EngagementScope(authorized_ip_ranges=("10.0.0.0/24",)), + sink, + ) + + decision = guard.check("command_generation", {"10.0.0.8"}) + + assert decision.allowed is True + assert sink.entries[0]["decision"] == "allow" + assert sink.entries[0]["targets"] == ["10.0.0.8"] + + +def test_guard_audits_deny_then_raises() -> None: + sink = MemoryAuditSink() + guard = ScopeGuard( + EngagementScope(authorized_ip_ranges=("10.0.0.0/24",)), + sink, + ) + + with pytest.raises(OutOfScopeError) as exc_info: + guard.check("command_execution", {"192.0.2.1"}) + + assert exc_info.value.offending_targets == {"192.0.2.1"} + assert sink.entries[0]["decision"] == "deny" + assert sink.entries[0]["offending_targets"] == ["192.0.2.1"] + + +def test_jsonl_sink_appends_allow_and_deny_records(tmp_path: Path) -> None: + path = tmp_path / "session" / "scope_audit.jsonl" + guard = ScopeGuard( + EngagementScope(authorized_hosts=frozenset({"lab.example.test"})), + JsonlAuditSink(path), + ) + + guard.check("generation", {"lab.example.test"}) + with pytest.raises(OutOfScopeError): + guard.check("execution", {"outside.example.test"}) + + entries = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()] + assert [entry["decision"] for entry in entries] == ["allow", "deny"] diff --git a/tests/integration/test_scope_guard_lifecycle.py b/tests/integration/test_scope_guard_lifecycle.py new file mode 100644 index 0000000..9300201 --- /dev/null +++ b/tests/integration/test_scope_guard_lifecycle.py @@ -0,0 +1,70 @@ +"""Integration coverage for scope validation across generation and execution.""" + +import json +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from pentestgpt.agents.command_generator import CommandGenerator +from pentestgpt.core.engagement import EngagementScope +from pentestgpt.core.exceptions import OutOfScopeError +from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard +from pentestgpt.orchestrator import PentestOrchestrator +from pentestgpt.utils.ptt_manager import PTTNode + + +@pytest.mark.asyncio +async def test_scope_guard_blocks_discovered_out_of_scope_host_before_llm_and_executor(tmp_path: Path) -> None: + router = MagicMock() + router.route_task.return_value = ( + '{"commands": [{"command": "nmap -sV 10.0.0.5"}]}', + "mock", + 0.0, + ) + executor = MagicMock() + scope = EngagementScope( + client="Example", + engagement_id="PT-9", + authorized_ip_ranges=("10.0.0.0/24",), + ) + audit_path = tmp_path / "scope_audit.jsonl" + guard = ScopeGuard(scope, JsonlAuditSink(audit_path)) + command_generator = CommandGenerator(model_router=router) + orchestrator = PentestOrchestrator( + model_router=router, + strategy_agent=MagicMock(), + command_generator=command_generator, + repetition_detector=MagicMock(), + results_verifier=MagicMock(), + tool_executor=executor, + mitre_planner=MagicMock(), + session_dir=str(tmp_path), + engagement_scope=scope, + scope_guard=guard, + ) + orchestrator.target_info = {"target": "10.0.0.5"} + task = PTTNode(id="1", name="Scan", status="to-do") + + commands = await orchestrator._generate_commands(task, orchestrator.target_info) + + assert commands + system_prompt = router.route_task.call_args.kwargs["system_prompt"] + assert "AUTHORIZED ENGAGEMENT CONTEXT" in system_prompt + assert "10.0.0.5" in system_prompt + + orchestrator._session_findings["discovered_hosts"].append("192.0.2.25") + router.reset_mock() + + with pytest.raises(OutOfScopeError): + await orchestrator._generate_commands(task, orchestrator.target_info) + router.route_task.assert_not_called() + + with pytest.raises(OutOfScopeError): + await orchestrator._dispatch_executor(commands) + executor.execute_batch_async.assert_not_called() + executor.execute_batch.assert_not_called() + + entries = [json.loads(line) for line in audit_path.read_text(encoding="utf-8").splitlines()] + assert any(entry["decision"] == "allow" for entry in entries) + assert any(entry["decision"] == "deny" and entry["offending_targets"] == ["192.0.2.25"] for entry in entries) diff --git a/tests/llms/test_router_cache_isolation.py b/tests/llms/test_router_cache_isolation.py new file mode 100644 index 0000000..25eba56 --- /dev/null +++ b/tests/llms/test_router_cache_isolation.py @@ -0,0 +1,41 @@ +"""Ensure routing-cache identity includes the LLM system prompt.""" + +from unittest.mock import MagicMock + +from pentestgpt.llms.router import ModelRouter, TaskType +from pentestgpt.utils.APIs.base_provider import LLMProvider + + +def test_same_message_with_different_system_prompts_uses_distinct_cache_entries() -> None: + provider = MagicMock(spec=LLMProvider) + provider.send_new_conversation.side_effect = [("first", "c1"), ("second", "c2")] + provider.count_tokens.return_value = 1 + provider.get_cost_estimate.return_value = 0.0 + router = ModelRouter( + {"provider:model": provider}, + routing_rules={TaskType.ANALYSIS: ["provider:model"]}, + ) + + first = router.route_task(TaskType.ANALYSIS, "same message", system_prompt="scope A") + second = router.route_task(TaskType.ANALYSIS, "same message", system_prompt="scope B") + + assert first[0] == "first" + assert second[0] == "second" + assert provider.send_new_conversation.call_count == 2 + assert len(router._routing_cache) == 2 + + +def test_identical_system_prompt_still_hits_cache() -> None: + provider = MagicMock(spec=LLMProvider) + provider.send_new_conversation.return_value = ("cached", "c1") + provider.count_tokens.return_value = 1 + provider.get_cost_estimate.return_value = 0.0 + router = ModelRouter( + {"provider:model": provider}, + routing_rules={TaskType.ANALYSIS: ["provider:model"]}, + ) + + router.route_task(TaskType.ANALYSIS, "same message", system_prompt="same scope") + router.route_task(TaskType.ANALYSIS, "same message", system_prompt="same scope") + + provider.send_new_conversation.assert_called_once() diff --git a/tests/prompts/test_authorization_preamble.py b/tests/prompts/test_authorization_preamble.py new file mode 100644 index 0000000..5213446 --- /dev/null +++ b/tests/prompts/test_authorization_preamble.py @@ -0,0 +1,49 @@ +"""Tests for authorization-aware prompt framing.""" + +from pentestgpt.core.engagement import EngagementScope +from pentestgpt.prompts.authorization import ( + build_authorization_preamble, + build_system_prompt, +) +from pentestgpt.prompts.mitre_prompts import MITREPrompts + + +def test_neutral_default_makes_no_positive_authorization_claim() -> None: + prompt = MITREPrompts.get_system_prompt() + + assert "AUTHORIZED ENGAGEMENT CONTEXT" not in prompt + assert "No engagement scope is configured" in prompt + + +def test_validated_target_set_asserts_authorized_engagement() -> None: + scope = EngagementScope( + client="Example Corp", + engagement_id="PT-7", + authorized_hosts=frozenset({"lab.example.test"}), + authorized_ip_ranges=("10.0.0.0/24",), + roe_notes="No denial-of-service testing", + ) + + prompt = build_system_prompt("You are a test role.", scope, {"lab.example.test", "10.0.0.5"}) + + assert "AUTHORIZED ENGAGEMENT CONTEXT" in prompt + assert "client=Example Corp" in prompt + assert "engagement_id=PT-7" in prompt + assert "10.0.0.5" in prompt + + +def test_out_of_scope_preamble_renders_refusal() -> None: + scope = EngagementScope(authorized_hosts=frozenset({"lab.example.test"})) + + preamble = build_authorization_preamble(scope, {"outside.example.test"}) + + assert "ENGAGEMENT SCOPE VIOLATION" in preamble + assert "Do not provide operational guidance" in preamble + + +def test_configured_scope_with_missing_targets_renders_refusal() -> None: + scope = EngagementScope(authorized_hosts=frozenset({"lab.example.test"})) + + preamble = build_authorization_preamble(scope) + + assert "requires at least one effective target" in preamble diff --git a/tests/prompts/test_prompt_templates.py b/tests/prompts/test_prompt_templates.py index e32f01d..e2ba678 100644 --- a/tests/prompts/test_prompt_templates.py +++ b/tests/prompts/test_prompt_templates.py @@ -190,6 +190,11 @@ def test_v3_static_methods_return_non_empty(self) -> None: assert len(PentestGPTPromptV3.get_generation_session_init().strip()) > 0 assert len(PentestGPTPromptV3.get_parsing_session_init().strip()) > 0 + def test_report_prompt_requires_all_fifteen_columns(self) -> None: + prompt = PentestGPTPrompt().report_generator_init + assert "15 elements" in prompt + assert "13 elements" not in prompt + # --------------------------------------------------------------------------- # 4. No placeholder artifacts like {TODO} or {{variable}} diff --git a/tests/prompts/test_route_task_coverage.py b/tests/prompts/test_route_task_coverage.py new file mode 100644 index 0000000..9c8425c --- /dev/null +++ b/tests/prompts/test_route_task_coverage.py @@ -0,0 +1,49 @@ +"""AST guard ensuring production route_task calls carry prompt framing.""" + +import ast +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +PACKAGE_ROOT = REPOSITORY_ROOT / "pentestgpt" +BENCHMARK_ALLOWLIST = {("utils/performance_benchmark.py", 260)} +PREAMBLE_BUILDERS = { + "get_system_prompt", + "build_system_prompt", + "_build_system_prompt", + "_authorization_system_prompt", +} + + +def test_every_route_task_call_has_a_preamble_bearing_system_prompt() -> None: + violations: list[str] = [] + calls = 0 + + for path in PACKAGE_ROOT.rglob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + relative = path.relative_to(PACKAGE_ROOT).as_posix() + for node in ast.walk(tree): + if not ( + isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "route_task" + ): + continue + calls += 1 + if (relative, node.lineno) in BENCHMARK_ALLOWLIST: + continue + system_prompt = next( + (keyword.value for keyword in node.keywords if keyword.arg == "system_prompt"), + None, + ) + if not isinstance(system_prompt, ast.Call): + violations.append(f"{relative}:{node.lineno} has no preamble builder") + continue + function = system_prompt.func + name = ( + function.attr + if isinstance(function, ast.Attribute) + else function.id if isinstance(function, ast.Name) else "" + ) + if name not in PREAMBLE_BUILDERS: + violations.append(f"{relative}:{node.lineno} uses {name or 'an unknown expression'}") + + assert calls == 40 + assert not violations, "\n".join(violations) diff --git a/tests/regression/test_critical_fixes.py b/tests/regression/test_critical_fixes.py index 3b2bf64..f0b313d 100644 --- a/tests/regression/test_critical_fixes.py +++ b/tests/regression/test_critical_fixes.py @@ -416,7 +416,7 @@ def test_cache_eviction_logic(self) -> None: # Manually add entries to cache to test eviction logic for i in range(5): - cache_key = (TaskType.STRATEGIC_PLANNING, f"test_message_{i}") + cache_key = (TaskType.STRATEGIC_PLANNING, f"test_message_{i}", "system_prompt_hash") router._routing_cache[cache_key] = (f"response_{i}", "test_provider", 0.01) # Trigger eviction if cache is full @@ -429,9 +429,9 @@ def test_cache_eviction_logic(self) -> None: # Check that oldest entries are removed (should have last 3 entries) keys = list(router._routing_cache.keys()) - assert (TaskType.STRATEGIC_PLANNING, "test_message_2") in keys - assert (TaskType.STRATEGIC_PLANNING, "test_message_3") in keys - assert (TaskType.STRATEGIC_PLANNING, "test_message_4") in keys + assert (TaskType.STRATEGIC_PLANNING, "test_message_2", "system_prompt_hash") in keys + assert (TaskType.STRATEGIC_PLANNING, "test_message_3", "system_prompt_hash") in keys + assert (TaskType.STRATEGIC_PLANNING, "test_message_4", "system_prompt_hash") in keys def test_cache_performance_metrics(self) -> None: """Test cache performance metrics tracking""" @@ -533,7 +533,7 @@ def test_model_router_complete_functionality(self) -> None: # Test 1: Cache limits work router._max_cache_size = 5 for i in range(10): - cache_key = (TaskType.STRATEGIC_PLANNING, f"test_{i}") + cache_key = (TaskType.STRATEGIC_PLANNING, f"test_{i}", "system_prompt_hash") router._routing_cache[cache_key] = ("response", "provider", 0.01) # Manually trigger eviction logic if cache exceeds limit diff --git a/tests/regression/test_fixes_quick.py b/tests/regression/test_fixes_quick.py index 8fc3581..1646069 100644 --- a/tests/regression/test_fixes_quick.py +++ b/tests/regression/test_fixes_quick.py @@ -56,7 +56,7 @@ def test_model_router() -> None: # Test cache limits router._max_cache_size = 3 for i in range(5): - cache_key = (TaskType.STRATEGIC_PLANNING, f"test_{i}") + cache_key = (TaskType.STRATEGIC_PLANNING, f"test_{i}", "system_prompt_hash") router._routing_cache[cache_key] = ("response", "provider", 0.01) # Manually trigger eviction diff --git a/tests/stealth/test_stealth_components.py b/tests/stealth/test_stealth_components.py index 5562bba..e4ec896 100644 --- a/tests/stealth/test_stealth_components.py +++ b/tests/stealth/test_stealth_components.py @@ -1187,8 +1187,9 @@ def test_error_recovery_scenarios(self) -> None: # Test with invalid session self.opsec_agent.current_session = None - is_valid, _reason = self.opsec_agent.validate_operation("test", {}) - assert is_valid # Should default to allowed when no session + is_valid, reason = self.opsec_agent.validate_operation("test", {}) + assert is_valid is False + assert "does not establish authorization" in reason def test_configuration_hot_reload(self) -> None: """Test hot reloading of configuration changes""" diff --git a/tests/utils/test_orchestrator_factory.py b/tests/utils/test_orchestrator_factory.py index 01de85b..1d67e39 100644 --- a/tests/utils/test_orchestrator_factory.py +++ b/tests/utils/test_orchestrator_factory.py @@ -13,12 +13,13 @@ from __future__ import annotations import sys -from typing import Any +from typing import Any, cast from unittest.mock import MagicMock, patch import pytest from pentestgpt.config.settings import AppSettings +from pentestgpt.core.engagement import EngagementScope from pentestgpt.orchestrator import PentestOrchestrator from pentestgpt.utils.dependency_injection import ( DependencyContainer, @@ -217,6 +218,31 @@ def test_core_agents_are_set( assert orchestrator.repetition_detector is not None assert orchestrator.results_verifier is not None + def test_engagement_scope_is_shared_by_factory_agents( + self, + mock_router: MagicMock, + mock_mitre_planner: MagicMock, + all_flags_off: AppSettings, + tmp_path: Any, + ) -> None: + scope = EngagementScope(authorized_ip_ranges=("10.0.0.0/24",)) + orchestrator = OrchestratorFactory().create_orchestrator( + model_router=mock_router, + mitre_planner=mock_mitre_planner, + session_dir=str(tmp_path), + engagement_scope=scope, + ) + + for agent in ( + orchestrator.strategy_agent, + orchestrator.command_generator, + orchestrator.repetition_detector, + orchestrator.results_verifier, + ): + scoped_agent = cast(Any, agent) + assert scoped_agent._engagement_scope is scope + assert scoped_agent._scope_guard is orchestrator.scope_guard + def test_rag_system_registered_and_forwarded( self, mock_router: MagicMock, From 261fefa37e957d9df1aed39c73b893e44dc4f5f9 Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:39:00 +0800 Subject: [PATCH 02/12] docs: document engagement scope enforcement --- docs/CHANGELOG.md | 37 +++++++++++++++++++- docs/api/README.md | 32 ++++++++++++++++++ docs/getting-started/README.md | 22 +++++++++++- docs/guides/INTEGRATION.md | 62 +++++++++++++++++++++++++++++----- docs/operations/SECURITY.md | 36 ++++++++++++++++++-- 5 files changed, 176 insertions(+), 13 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1feb103..9b68a7c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,40 @@ All notable changes to PentestGPT will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Canonical engagement authorization scope** (`pentestgpt/core/engagement.py`) — normalizes hostname, URL, IP, port, + and CIDR inputs and validates every effective target against one `EngagementScope` shared by prompts, the + orchestrator, agents, exploitation, and OPSEC. +- **Session-independent scope guard and audit trail** (`pentestgpt/core/scope_guard.py`) — records every allow or deny + decision in append-only `scope_audit.jsonl` entries, including phase, targets, offending targets, scope identity, and + reason. +- **Engagement settings** — `ENGAGEMENT_CLIENT`, `ENGAGEMENT_ID`, `AUTHORIZED_HOSTS`, `AUTHORIZED_IP_RANGES`, and + `RULES_OF_ENGAGEMENT` can configure the scope through `.env`. + +### Changed + +- **Authorization-aware system prompts** — LLM system prompts now use centralized role text and neutral framing by + default. A positive authorization statement is emitted only after every effective target passes configured-scope + validation. +- **Fail-closed orchestration** — configured engagements are checked at session start, before command or exploit + generation, and before synchronous or asynchronous tool execution. Discovered hosts join the effective target set; any + missing or out-of-scope target blocks the operation before the LLM or executor is invoked. +- **OPSEC scope propagation** — `POST /stealth/sessions` parses `authorized_scope` into the canonical model and attaches + it to the OPSEC session. A missing OPSEC session no longer produces an authorizing result. +- **LLM router cache isolation** — cache keys now include system-prompt identity so responses produced under different + authorization contexts cannot share an entry. + +### Security + +- Unconfigured local or lab workflows remain available with neutral prompt framing and an audited, non-authorizing allow + decision. Once either authorized target list is configured, an empty target set or any out-of-scope member is denied + and audited. + +--- + ## [3.4.0] - 2026-06-19 ### Exploitation Capability Improvements — Phases 1–12 @@ -853,7 +887,8 @@ PentestGPT follows [Semantic Versioning](https://semver.org/): - OpenAI, Anthropic, xAI, and Ollama for LLM platforms - The cybersecurity community for tools and knowledge -[Unreleased]: https://github.com/SALhik/AutoPentester/compare/v3.3.0...HEAD +[Unreleased]: https://github.com/SALhik/AutoPentester/compare/v3.4.0...HEAD +[3.4.0]: https://github.com/SALhik/AutoPentester/compare/v3.3.0...v3.4.0 [3.3.0]: https://github.com/SALhik/AutoPentester/compare/v3.2.1...v3.3.0 [3.2.1]: https://github.com/SALhik/AutoPentester/compare/v3.2.0...v3.2.1 [3.2.0]: https://github.com/SALhik/AutoPentester/compare/v3.1.0...v3.2.0 diff --git a/docs/api/README.md b/docs/api/README.md index 39869df..0779fdf 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -439,6 +439,38 @@ runtime. | POST | `/stealth/metrics` | Record a performance metric | | GET | `/stealth/compliance/report` | Generate a compliance report | +### OPSEC Session Authorization Scope + +`POST /stealth/sessions` accepts an `authorized_scope` object and converts it into the same canonical `EngagementScope` +used by the orchestrator and prompt layer. Use the canonical field names shown below: + +```json +{ + "name": "Quarterly external assessment", + "description": "Approved production perimeter test", + "target_info": { + "hostname": "edge.example.test" + }, + "authorized_scope": { + "client": "Example Corp", + "engagement_id": "PT-2026-014", + "authorized_hosts": ["edge.example.test"], + "authorized_ip_ranges": ["203.0.113.0/28"], + "roe_notes": "Testing window 09:00-17:00 UTC" + } +} +``` + +`authorized_hosts` contains exact hostnames or IP literals; `authorized_ip_ranges` contains IPv4 or IPv6 CIDRs. +Hostnames are case-normalized, URLs are reduced to their host component, ports are removed, and IPs are checked for CIDR +membership. For compatibility, the parser also accepts `hosts` or `targets` for the host list, `ip_ranges` or `networks` +for CIDRs, and `rules_of_engagement` for `roe_notes`. + +The request's `target_info` is operational metadata and does not grant authorization by itself. If both scope target +lists are empty, subsequent checks use neutral, non-authorizing behavior. Once either list is populated, every effective +target must validate in scope; a missing or out-of-scope target prevents the scope-aware generation or operation from +continuing and writes a deny entry to `scope_audit.jsonl`. + --- ## Testing Framework API diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 7e4cff3..0c91e76 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -28,7 +28,7 @@ pip install -e . cp .env.example .env ``` -1. Add at least one API key: +2. Add at least one API key: ```bash # In .env, set one or more of: @@ -37,6 +37,26 @@ ANTHROPIC_API_KEY=your_key_here XAI_API_KEY=your_key_here ``` +3. For an authorized engagement, configure the permitted targets: + +```bash +# Exact hostnames or IP addresses +AUTHORIZED_HOSTS=["lab.example.test"] + +# Networks containing authorized IP targets +AUTHORIZED_IP_RANGES=["10.10.10.0/24"] + +# Optional audit and prompt context +ENGAGEMENT_CLIENT=Example Corp +ENGAGEMENT_ID=PT-2026-014 +RULES_OF_ENGAGEMENT=Testing window 09:00-17:00 UTC +``` + +Both target lists are empty by default. In that state, local and lab workflows remain available, but system prompts use +neutral framing and do not claim that authorization has been verified. Once either list is configured, every effective +target—including hosts discovered during the session—must be in scope before LLM generation or tool execution can +continue. Each decision is appended to `scope_audit.jsonl` under the configured session directory. + For full configuration options, see the [Configuration Reference](../configuration/README.md). ## Basic Usage diff --git a/docs/guides/INTEGRATION.md b/docs/guides/INTEGRATION.md index 19cfb14..0b29aa4 100644 --- a/docs/guides/INTEGRATION.md +++ b/docs/guides/INTEGRATION.md @@ -18,17 +18,19 @@ ## Overview -PentestGPT v3.0 integrates multiple specialized subsystems into a unified penetration testing platform. Three real +PentestGPT v3.0 integrates multiple specialized subsystems into a unified penetration testing platform. Four real coordination points hold the system together: -| Module | Location | Role | -| ----------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ | -| `AppContext` | `pentestgpt/core/app_context.py` | Singleton holding app-wide state: config, db session, named manager references | -| `AgentCommunicationHub` | `pentestgpt/communication/agent_communication.py` | In-process inter-agent message bus (pub/sub, request/response, broadcast) | -| `OrchestratorFactory` | `pentestgpt/utils/orchestrator_factory.py` | DI factory that wires agents, optional subsystems, and `PentestOrchestrator` | +| Module | Location | Role | +| -------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ | +| `AppContext` | `pentestgpt/core/app_context.py` | Singleton holding app-wide state: config, db session, named manager references | +| `EngagementScope` / `ScopeGuard` | `pentestgpt/core/` | Canonical target authorization, enforcement, and decision auditing | +| `AgentCommunicationHub` | `pentestgpt/communication/agent_communication.py` | In-process inter-agent message bus (pub/sub, request/response, broadcast) | +| `OrchestratorFactory` | `pentestgpt/utils/orchestrator_factory.py` | DI factory that wires agents, optional subsystems, and `PentestOrchestrator` | -> **Note:** `pentestgpt/core/` contains only `app_context.py` and `exceptions.py`. There is no -> `integration_controller.py`, `message_bus.py`, `service_registry.py`, or `orchestrator_api.py` in that package. +> **Note:** `pentestgpt/core/` contains the application context, canonical engagement scope, scope guard, and shared +> exceptions. There is no `integration_controller.py`, `message_bus.py`, `service_registry.py`, or `orchestrator_api.py` +> in that package. ### High-Level Architecture @@ -39,6 +41,9 @@ User / API PentestOrchestrator (pentestgpt/orchestrator.py) | created by OrchestratorFactory (pentestgpt/utils/orchestrator_factory.py) | + +-- EngagementScope + ScopeGuard + | -- validates targets and appends scope_audit.jsonl + | +-- StrategyAgent +-- RepetitionDetector +-- CommandGenerator @@ -217,6 +222,47 @@ orchestrator = factory.create_orchestrator( ) ``` +### Engagement Authorization Lifecycle + +`OrchestratorFactory` is also the assembly point for engagement authorization. It builds one `EngagementScope` from +`AppSettings` and one `ScopeGuard` for the session, then injects that same pair into the orchestrator, core agents, and +the exploitation chain. New construction paths should extend this factory instead of constructing competing scope or +guard instances. + +An integration may override the environment-backed scope explicitly: + +```python +from pentestgpt.core.engagement import EngagementScope + +scope = EngagementScope( + client="Example Corp", + engagement_id="PT-2026-014", + authorized_hosts=frozenset({"lab.example.test"}), + authorized_ip_ranges=("10.10.10.0/24",), + roe_notes="Testing window 09:00-17:00 UTC", +) + +orchestrator = factory.create_orchestrator( + model_router=router, + mitre_planner=planner, + session_dir="sessions/PT-2026-014", + engagement_scope=scope, +) +``` + +The lifecycle is fail-closed only after a scope is configured: + +| State or phase | Behavior | +| --------------------------------------------- | ------------------------------------------------------------------------------------------ | +| Both authorized target lists are empty | Allow local/lab use with neutral framing; do not assert authorization | +| Session start | Validate the primary effective target | +| LLM command or exploit generation | Validate the primary target plus targets extracted from findings and task context | +| Tool or exploit execution | Validate again immediately before every synchronous, asynchronous, or direct executor path | +| Any configured-scope target missing or denied | Raise `OutOfScopeError`, skip the LLM/executor call, and write a deny audit entry | + +All allow and deny decisions are appended to `/scope_audit.jsonl`. The guard includes discovered hosts in +the effective target set, preventing later iterations from silently expanding beyond the original authorization. + ### Feature-Flagged Subsystems `OrchestratorFactory.create_orchestrator()` reads `AppSettings` feature flags and conditionally instantiates optional diff --git a/docs/operations/SECURITY.md b/docs/operations/SECURITY.md index 1b599bc..4020e06 100644 --- a/docs/operations/SECURITY.md +++ b/docs/operations/SECURITY.md @@ -11,9 +11,10 @@ 7. [Compliance Monitoring System](#compliance-monitoring-system) 8. [Usage Guide](#usage-guide) 9. [Integration Guide](#integration-guide) -10. [Best Practices](#best-practices) -11. [Troubleshooting](#troubleshooting) -12. [API Reference](#api-reference) +10. [Engagement Scope Audit Log](#engagement-scope-audit-log) +11. [Best Practices](#best-practices) +12. [Troubleshooting](#troubleshooting) +13. [API Reference](#api-reference) ## Introduction @@ -318,6 +319,35 @@ audit_results = auditor.perform_comprehensive_audit(include_dependency_scan=True # - audit_results["tracked_vulnerabilities"] — VulnerabilityTracker priority list ``` +## Engagement Scope Audit Log + +The authorization `ScopeGuard` records every scope decision independently of the optional OPSEC session. Factory-built +orchestrators append JSON Lines records to `/scope_audit.jsonl`; an OPSEC agent created directly without a +factory guard uses `sessions//scope_audit.jsonl`. The file and its parent directory are created on the +first scope check. + +Each line is one JSON object with these fields: + +| Field | Meaning | +| ------------------- | -------------------------------------------------------------- | +| `timestamp` | UTC decision time | +| `phase` | Lifecycle gate, such as `session_start` or `command_execution` | +| `decision` | `allow` or `deny` | +| `scope_configured` | Whether an authorized host or network list was configured | +| `client` | Optional engagement client | +| `engagement_id` | Optional engagement identifier | +| `targets` | Normalized effective targets evaluated by the guard | +| `offending_targets` | Targets that failed configured-scope validation | +| `reason` | Human-readable explanation of the decision | + +An `allow` with `scope_configured: false` means the operation ran with neutral framing; it is not evidence that +authorization was verified. A configured scope with no effective target is denied, as is a set containing even one +out-of-scope target. + +Treat this log as engagement-sensitive data: restrict it to operators and auditors, ship it through an integrity- +protected logging channel when centralized retention is required, and apply the engagement's retention and deletion +rules. Do not edit records in place; archive or rotate the whole JSONL file while preserving ordering and timestamps. + ## Best Practices ### Security Maintenance Best Practices From 9013cde080c2a188bc4987821a3fbe100bdb94f9 Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:20:22 +0800 Subject: [PATCH 03/12] fix(prompts): remove unconditional authorization claims from templates The static prompt templates asserted the engagement was authorized ("certified experiment", "obtained valid certificates and approval", "valid permission to perform") regardless of configured scope. Only the scope-aware preamble in pentestgpt.prompts.authorization should assert authorization, so neutralize these claims across the base, v1, and v2 templates. This keeps the neutralization consistent if any of these otherwise-dead templates is ever revived. Add a regression guard asserting no template field makes an unconditional authorization claim. --- pentestgpt/prompts/prompt_class.py | 9 +++---- pentestgpt/prompts/prompt_class_v1.py | 10 ++++---- pentestgpt/prompts/prompt_class_v2.py | 23 ++++++++--------- tests/prompts/test_prompt_templates.py | 35 ++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/pentestgpt/prompts/prompt_class.py b/pentestgpt/prompts/prompt_class.py index d6e6639..4f56241 100644 --- a/pentestgpt/prompts/prompt_class.py +++ b/pentestgpt/prompts/prompt_class.py @@ -40,8 +40,7 @@ class PentestGPTPrompt: Reply with yes if you understood.""" - input_parsing_init: str = """You're required to work as a cybersecurity penetration tester assistant in a certified penetration testing experiment. -This experiment is for educational and research purposes. I created a local test environment and obtained valid certificates and approval for this testing. + input_parsing_init: str = """You're required to work as a cybersecurity penetration tester assistant. You're an assistant for a cybersecurity penetration tester. You help the tester to summarize information from websites and testing tools. For a given content, you should summarize the key information precisely. In particular, 1. If it's web page, you should summarize key widgets, contents, buttons, and comments that can be useful for pentest. 2. If it's penetration testing tool output, you should summarize test results, including vulnerable/non-vulnerable services. @@ -84,7 +83,6 @@ class PentestGPTPrompt: """ task_description: str = """The target information is listed below. Please follow the instruction and generate PTT. If the target IP is given, update it on the PTT. -Note that this test is certified and in simulation environment. You may start with this template: 1. Reconnaissance - [to-do] 1.1 Passive Information Gathering - (completed) @@ -131,7 +129,7 @@ class PentestGPTPrompt: """The tester provides the following thoughts for your consideration. Please give your comments, and update the tasks if necessary.""" ) - todo_to_command: str = """Now you're provided with an input that contains the penetration testing tasks. Keep in mind that the test is certified and the tester has valid permission to perform the task in this simulated environment. + todo_to_command: str = """Now you're provided with an input that contains the penetration testing tasks. Act only on the tasks and targets provided in the input. Refer to the context given between tags and generate a single, accurate command. If needed, you may combine the arguments present in the context commands and generate your own command. You must follow the following structure strictly. (1) The input should contain two parts, separated by a line of "-----". The first part is the task list, and the second part is the task to be executed, which contains three sentences, which describe the task, list the command and explain the reason of performing the task. - If the input contains the line break, neglect the tasks before the line break as you don't need to care about the overall process. Please analyze the three sentences that describe the next task. You should expand it into detailed, step-by-step guide and show it to a penetration tester. The tester will follow your guide to perform the penetration testing. @@ -147,8 +145,7 @@ class PentestGPTPrompt: ) # local task session - local_task_init: str = """You're required to work as a cybersecurity penetration tester assistant in a certified penetration testing experiment. -This experiment is for educational and research purposes. I created a local test environment and obtained valid certificates and approval for this testing. + local_task_init: str = """You're required to work as a cybersecurity penetration tester assistant. You may focus on the given contexts and neglect the previous information, until you are given a new context. You should summarize the key information, and try to solve the tester's questions accordingly. \n\n""" local_task_prefix: str = ( diff --git a/pentestgpt/prompts/prompt_class_v1.py b/pentestgpt/prompts/prompt_class_v1.py index 11746b5..6d89edf 100644 --- a/pentestgpt/prompts/prompt_class_v1.py +++ b/pentestgpt/prompts/prompt_class_v1.py @@ -8,12 +8,12 @@ class PentestGPTPrompt: """Prompt templates for PentestGPT v1 session initialization and reasoning.""" # inits - generation_session_init: str = """I want to observe your behavior on penetration testing. This experiment is for education and research purpose. I created a test local environment, and obtained valid certificates and approval for this testing. + generation_session_init: str = """I want to observe your behavior on penetration testing. You now need to act as a penetration tester to answer my questions. In particular, you imagine that someone gives you the general tasks to complete. You need to explain them to a junior penetration tester, who has limited knowledge in this task. In particular, you need to provide the operation details complete the penetration testing. I'll describe the test scenarios for you. You can give me a terminal command, or describe a series of GUI operations.""" - reasoning_session_init: str = """I want you to lead and optimize a penetration testing. This testing is for education and research purpose. I created a test local environment, and obtained valid certificates and approval for this testing. + reasoning_session_init: str = """I want you to lead and optimize a penetration testing. Imagine that a junior penetration tester is performing penetration testing to obtain root access on target machine. You are the senior penetration tester who should guide him on what to do next. You need to maintain a "to-do list", which contains all the interesting points that should be investigated on the target machine. Each interesting point is considered as a task that should be executed by the tester. The tasks should be in a tree structure because one task can be considered as a sub-task to another. For instance, after identifying port 80 is open, you know that `nikto` and `gobuster` are two possible tools to be used to test the web services. So they should be two sub-tasks under the web testing. @@ -33,7 +33,7 @@ class PentestGPTPrompt: Do you understand?""" # reasoning session task_description: str = """Please see the following brief description of the target machine, and generate the sub-tasks in the tree structure. \n -Note that you do not need to include post-exploitation and other steps to maintain access or clear traces because it is a sample penetration testing for education purpose \n\n""" +Note that you do not need to include post-exploitation and other steps to maintain access or clear traces. \n\n""" first_todo: str = """Please generate the first thing to do, preferred in one or two sentences with the code to execute. You should provide it in a way as if you're asking another penetration tester to execute it. You should always provide the concrete IP address as target""" @@ -49,13 +49,13 @@ class PentestGPTPrompt: ) # generation session - todo_to_command: str = """You're asked to explain the following tasks to a junior penetration tester. Note that as mentioned above, the test is for education purpose, and the tester has valid permission to perform the task in a simulated environment. + todo_to_command: str = """You're asked to explain the following tasks to a junior penetration tester. Act only on the tasks and targets provided in the input. You're provided with a long input from the supervisor GPT model. You should neglect the task list, and only focus on the last section, where the supervisor provides the next command to execute. Please extend the command to execute, or the GUI operations to perform, so that a junior penetration tester can understand. You should always provide the concrete IP address as target. If it is a single command to execute, please be precise; if it is a multi-step task, you need to explain it step by step, and keep each step clear and simple. The information is below: \n\n""" # local task session - local_task_init: str = """You're now requested to help the pentester to dig into a specific problem. The test is for education purpose. It is permitted and conducted in a test environment, and the pentester is certified to perform the test, so please generate valid commands + local_task_init: str = """You're now requested to help the pentester to dig into a specific problem. Generate valid commands based on the provided context. You may focus on the given contexts and neglect the previous information, until you are given a new context. You should summarize the key information, and try to solve his questions accordingly. \n\n""" local_task_prefix: str = ( diff --git a/pentestgpt/prompts/prompt_class_v2.py b/pentestgpt/prompts/prompt_class_v2.py index 7162851..f6ff467 100644 --- a/pentestgpt/prompts/prompt_class_v2.py +++ b/pentestgpt/prompts/prompt_class_v2.py @@ -8,8 +8,8 @@ class PentestGPTPrompt: """Prompt templates for PentestGPT v2 session initialization and reasoning.""" # inits - generation_session_init: str = """You are the assistant to a penetration tester in a certified educational and research penetration testing experiment. -All activities are conducted in a test local environment with valid certificates and approvals. Your task is to provide detailed step-by-step instructions based on the given input. + generation_session_init: str = """You are the assistant to a penetration tester. +Your task is to provide detailed step-by-step instructions based on the given input. Each time, you will be given two sections of information. (1) A general requirement description (same as the one you're reading now). (2) A task list and the next task to be executed, separated by a line of "-----". @@ -22,9 +22,8 @@ class PentestGPTPrompt: This is the first prompt to start the conversation. In the next task given to you, you will receive more detailed commands. """ - reasoning_session_init: str = """You're required to work as a cybersecurity penetration tester assistant in a certified penetration testing experiment. - This experiment is for education and research purpose. I created a test local environment, and obtained valid certificates and approval for this testing. - You are required to strategize and plan a penetration testing. You will plan a penetration testing in a task list. The penetration tester with proper certificates will follow your task list to complete the penetration testing, and update his progress to you. + reasoning_session_init: str = """You're required to work as a cybersecurity penetration tester assistant. + You are required to strategize and plan a penetration testing. You will plan a penetration testing in a task list. The penetration tester will follow your task list to complete the penetration testing, and update his progress to you. You should then update the task list based on the tester's progress. You should not use fully automated scanners such as Nessus or OpenVAS. You should use manual penetration testing tools such as nmap, nikto, gobuster, etc. You answer should follow the following requirements: 1. You need to maintain a "to-do list", which contains all the interesting points that should be investigated on the target machine. Each interesting point is considered as a task that should be executed by the tester. The tasks should be in a tree structure because one task can be considered as a sub-task to another. @@ -43,7 +42,7 @@ class PentestGPTPrompt: 3. Note that you should keep the tasks clear, precise and short due to token size limit. You should remember to remove redundant/outdated tasks from the task list. The detailed tasks and the user inputs will be provided shortly """ - input_parsing_init: str = """You are working as an assistant to a cybersecurity penetration tester in a certified penetration testing experiment. Your role is to summarize key information from websites and testing tools in a concise and precise manner. + input_parsing_init: str = """You are working as an assistant to a cybersecurity penetration tester. Your role is to summarize key information from websites and testing tools in a concise and precise manner. For web pages, please summarize the key widgets, contents, buttons, and comments that can be useful for penetration testing. For penetration testing tools, please summarize the test results, including which services are vulnerable and which services are not vulnerable. You should include both the field name and value for each finding, such as the port number and service name/version for open ports. @@ -54,7 +53,7 @@ class PentestGPTPrompt: # reasoning session task_description: str = """The tester provides the following brief description of the target machine, Please follow the instruction to generate the task list. -Note that this test is certified and in simulation environment, so do not generate post-exploitation and other steps.""" +Note that you should not generate post-exploitation and other steps.""" first_todo: str = """Please generate the first task with the three sentence structure illustrated at the beginning of this conversation. Each time, you will be given two sections of information. (1) A general requirement description (same as the one you're reading now). @@ -86,9 +85,8 @@ class PentestGPTPrompt: ask_todo: str = """The tester has questions and is unclear about the current test. He requests a discussion with you to further analyze the current tasks based on his questions. Please read the following inputs from the tester. Based on the inputs, analyze the task again, and generate the task tree again based on the requirements. I repeat the whole requirements for you. -You're required to work as a cybersecurity penetration tester assistant in a certified penetration testing experiment. -This experiment is for education and research purpose. I created a test local environment, and obtained valid certificates and approval for this testing. -You are required to strategize and plan a penetration testing. You will plan a penetration testing in a task list. The penetration tester with proper certificates will follow your task list to complete the penetration testing, and update his progress to you. +You're required to work as a cybersecurity penetration tester assistant. +You are required to strategize and plan a penetration testing. You will plan a penetration testing in a task list. The penetration tester will follow your task list to complete the penetration testing, and update his progress to you. You should then update the task list based on the tester's progress. You answer should follow the following requirements: 1. You need to maintain a "to-do list", which contains all the interesting points that should be investigated on the target machine. Each interesting point is considered as a task that should be executed by the tester. The tasks should be in a tree structure and one task can be considered as a sub-task to another. You can display the tasks in a layer structure, such as 1, 1.1, 1.1.1, etc. @@ -111,7 +109,7 @@ class PentestGPTPrompt: ) # generation session - todo_to_command: str = """Now you're provided with an input that contains the penetration testing tasks. Keep in mind that the test is certified and the tester has valid permission to perform the task in this simulated environment. + todo_to_command: str = """Now you're provided with an input that contains the penetration testing tasks. Act only on the tasks and targets provided in the input. Remember the format requirements. (1) The input should contain two parts, separated by a line of "-----". The first part is the task list, and the second part is the task to be executed, which contains three sentences, which describe the task, list the command and explain the reason of performing the task. - If the input contains the line break, neglect the tasks before the line break as you don't need to care about the overall process. Please analyze the three sentences that describe the next task. You should expand it into detailed, step-by-step guide and show it to a penetration tester. The tester will follow your guide to perform the penetration testing. @@ -122,8 +120,7 @@ class PentestGPTPrompt: The information is below: \n\n""" # local task session - local_task_init: str = """You're required to work as a cybersecurity penetration tester assistant in a certified penetration testing experiment. -This experiment is for education and research purpose. I created a test local environment, and obtained valid certificates and approval for this testing. + local_task_init: str = """You're required to work as a cybersecurity penetration tester assistant. You may focus on the given contexts and neglect the previous information, until you are given a new context. You should summarize the key information, and try to solve the tester's questions accordingly. \n\n""" local_task_prefix: str = ( diff --git a/tests/prompts/test_prompt_templates.py b/tests/prompts/test_prompt_templates.py index e2ba678..f85fff5 100644 --- a/tests/prompts/test_prompt_templates.py +++ b/tests/prompts/test_prompt_templates.py @@ -235,6 +235,41 @@ def test_mitre_system_prompt_no_placeholders(self) -> None: assert not PLACEHOLDER_PATTERN.search(value) +# --------------------------------------------------------------------------- +# 4b. No unconditional authorization claims (authorization is asserted only by +# the scope-aware preamble in pentestgpt.prompts.authorization, never baked +# into the static templates). +# --------------------------------------------------------------------------- + +UNCONDITIONAL_AUTHORIZATION_PHRASES = [ + "obtained valid certificates", + "valid certificates and approval", + "valid permission to perform", + "certified to perform", + "test is certified", + "certified penetration testing experiment", + "certified educational and research", + "with proper certificates", + "it is permitted", +] + + +class TestNoUnconditionalAuthorizationClaims: + """Static templates must not assert authorization unconditionally.""" + + @pytest.mark.parametrize( + "cls", + [PentestGPTPrompt, PentestGPTPromptV1, PentestGPTPromptV2], + ids=["base", "v1", "v2"], + ) + def test_dataclass_prompts_make_no_authorization_claim(self, cls: type) -> None: + instance = cls() + for f in dataclasses.fields(instance): + value = getattr(instance, f.name).lower() + for phrase in UNCONDITIONAL_AUTHORIZATION_PHRASES: + assert phrase not in value, f"{cls.__name__}.{f.name} asserts authorization: {phrase!r}" + + # --------------------------------------------------------------------------- # 5. MITREPrompts.get_system_prompt() returns non-empty string # --------------------------------------------------------------------------- From 097d81f70b13da3782e54cf6e8dffb5181d0ec78 Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:20:36 +0800 Subject: [PATCH 04/12] fix(opsec): route scope audit log to the configured session directory OPSECAgent.create_session hardcoded the "sessions" directory for its scope audit sink, ignoring the orchestrator's configured session_dir on the standalone path. Add a session_dir parameter (default "sessions") and propagate the orchestrator's value so scope audit logs land alongside the rest of the session state. --- pentestgpt/agents/opsec_agent.py | 12 ++++++++++-- pentestgpt/orchestrator.py | 2 +- tests/agents/test_opsec_agent.py | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pentestgpt/agents/opsec_agent.py b/pentestgpt/agents/opsec_agent.py index f4b1433..b1232d7 100644 --- a/pentestgpt/agents/opsec_agent.py +++ b/pentestgpt/agents/opsec_agent.py @@ -479,16 +479,24 @@ class OPSECAgent(BaseAgent): for penetration testing operations. """ - def __init__(self, model_router: ModelRouter, config: StealthConfig, **kwargs: Any): + def __init__( + self, + model_router: ModelRouter, + config: StealthConfig, + session_dir: str = "sessions", + **kwargs: Any, + ): """Initialize OPSEC Agent. Args: model_router: ModelRouter for LLM access config: Stealth configuration + session_dir: Base directory for session-scoped audit logs """ super().__init__(**kwargs) self.router = model_router self.config = config + self.session_dir = session_dir self.current_session: OPSECSession | None = None self.session_history: list[OPSECSession] = [] self.database_manager: Any = None @@ -526,7 +534,7 @@ def create_session(self, scope: EngagementScope | None = None) -> str: if self._scope_guard is None: self._scope_guard = ScopeGuard( self._engagement_scope, - JsonlAuditSink(Path("sessions") / session_id / "scope_audit.jsonl"), + JsonlAuditSink(Path(self.session_dir) / session_id / "scope_audit.jsonl"), ) elif self._scope_guard.scope != self._engagement_scope: self._scope_guard = ScopeGuard(self._engagement_scope, self._scope_guard.audit_sink) diff --git a/pentestgpt/orchestrator.py b/pentestgpt/orchestrator.py index 9d24179..6e634dc 100644 --- a/pentestgpt/orchestrator.py +++ b/pentestgpt/orchestrator.py @@ -136,7 +136,7 @@ def __init__( if not opsec_agent: config = StealthConfig() - self.opsec_agent = OPSECAgent(model_router, config) + self.opsec_agent = OPSECAgent(model_router, config, session_dir=session_dir) else: self.opsec_agent = opsec_agent diff --git a/tests/agents/test_opsec_agent.py b/tests/agents/test_opsec_agent.py index ad6cc4b..fb2f82b 100644 --- a/tests/agents/test_opsec_agent.py +++ b/tests/agents/test_opsec_agent.py @@ -1,6 +1,7 @@ """Tests for OPSECAgent — environment profiling, compliance, session management.""" import json +from pathlib import Path from typing import Any from unittest.mock import MagicMock @@ -16,6 +17,7 @@ StealthConfig, StealthLevel, ) +from pentestgpt.core.scope_guard import JsonlAuditSink from tests import make_mock_router # --------------------------------------------------------------------------- @@ -297,6 +299,18 @@ def test_create_session_adds_to_history(self) -> None: agent.create_session() assert len(agent.session_history) == 1 + def test_create_session_audit_uses_configured_session_dir(self, tmp_path: Any) -> None: + agent = OPSECAgent( + model_router=make_mock_router(route_task_return=("Mocked response", "openai", 0.01)), + config=StealthConfig(), + session_dir=str(tmp_path), + ) + session_id = agent.create_session() + assert agent._scope_guard is not None + sink = agent._scope_guard.audit_sink + assert isinstance(sink, JsonlAuditSink) + assert sink.path == Path(tmp_path) / session_id / "scope_audit.jsonl" + def test_end_current_session_clears_current(self) -> None: agent = _make_opsec_agent() agent.create_session() From 6f5ca19d8c2e6c8a6266b759089a4cdcc8ac1fa8 Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:15:29 +0800 Subject: [PATCH 05/12] chore(coderabbit): exclude docs/ from review for PR #15 CodeRabbit exceeds its per-PR file limit on this branch. Temporarily skip the docs/ folder so it can review the code changes. Revert once the review is complete. --- .coderabbit.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..599a16b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# TEMPORARY (PR #15): exclude docs/ so CodeRabbit stays under its per-PR file +# limit. Revert this file once the review is complete. +reviews: + path_filters: + - "!docs/**" From 31d0dee2571beae837f1ff2306741d962f24bfe0 Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:26:08 +0800 Subject: [PATCH 06/12] Fix scope enforcement review findings --- docs/api/README.md | 3 + pentestgpt/agents/exploit_developer.py | 4 + pentestgpt/agents/opsec_agent.py | 13 +- pentestgpt/api/stealth_api.py | 2 + pentestgpt/core/engagement.py | 112 +++++++++++++++--- pentestgpt/core/scope_guard.py | 10 +- pentestgpt/orchestrator.py | 13 +- pentestgpt/prompts/prompt_class.py | 5 +- pentestgpt/utils/dependency_injection.py | 11 +- pentestgpt/utils/orchestrator_factory.py | 2 + tests/agents/test_exploit_scope.py | 18 +++ tests/agents/test_opsec_agent.py | 25 +++- tests/api/test_opsec_session_scope.py | 18 ++- tests/core/test_engagement.py | 36 ++++++ .../integration/test_scope_guard_lifecycle.py | 27 +++++ tests/prompts/test_prompt_templates.py | 10 +- tests/utils/test_orchestrator_factory.py | 23 ++++ 17 files changed, 289 insertions(+), 43 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index 0779fdf..2f2393d 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -466,6 +466,9 @@ Hostnames are case-normalized, URLs are reduced to their host component, ports a membership. For compatibility, the parser also accepts `hosts` or `targets` for the host list, `ip_ranges` or `networks` for CIDRs, and `rules_of_engagement` for `roe_notes`. +Malformed host values, CIDRs, or scope field types are rejected with HTTP 422 and the sanitized response +`{"detail":"Invalid authorized scope"}`. + The request's `target_info` is operational metadata and does not grant authorization by itself. If both scope target lists are empty, subsequent checks use neutral, non-authorizing behavior. Once either list is populated, every effective target must validate in scope; a missing or out-of-scope target prevents the scope-aware generation or operation from diff --git a/pentestgpt/agents/exploit_developer.py b/pentestgpt/agents/exploit_developer.py index 597f909..bb56fa1 100644 --- a/pentestgpt/agents/exploit_developer.py +++ b/pentestgpt/agents/exploit_developer.py @@ -227,6 +227,8 @@ def develop_and_test_exploits( return {"attempted": attempted_exploits, "successful": successful_exploits} + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error developing and testing exploits: {e}") return {"attempted": [], "successful": []} @@ -304,6 +306,8 @@ def _generate_exploit_candidate( return candidate + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error generating exploit candidate: {e}") return None diff --git a/pentestgpt/agents/opsec_agent.py b/pentestgpt/agents/opsec_agent.py index b1232d7..bd07b2c 100644 --- a/pentestgpt/agents/opsec_agent.py +++ b/pentestgpt/agents/opsec_agent.py @@ -531,13 +531,8 @@ def create_session(self, scope: EngagementScope | None = None) -> str: # Create session with current configuration if scope is not None: self._engagement_scope = scope - if self._scope_guard is None: - self._scope_guard = ScopeGuard( - self._engagement_scope, - JsonlAuditSink(Path(self.session_dir) / session_id / "scope_audit.jsonl"), - ) - elif self._scope_guard.scope != self._engagement_scope: - self._scope_guard = ScopeGuard(self._engagement_scope, self._scope_guard.audit_sink) + audit_sink = JsonlAuditSink(Path(self.session_dir) / session_id / "scope_audit.jsonl") + self._scope_guard = ScopeGuard(self._engagement_scope, audit_sink) session = OPSECSession(session_id, self.config.opsec, self._engagement_scope) session.start_session() @@ -617,6 +612,8 @@ def analyze_target_environment(self, target_info: dict[str, Any]) -> dict[str, A return analysis + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error in target environment analysis: {e}") return { @@ -839,6 +836,8 @@ def get_opsec_recommendations( return recommendations + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error generating OPSEC recommendations: {e}") return { diff --git a/pentestgpt/api/stealth_api.py b/pentestgpt/api/stealth_api.py index 703cb4a..d581419 100644 --- a/pentestgpt/api/stealth_api.py +++ b/pentestgpt/api/stealth_api.py @@ -337,6 +337,8 @@ async def create_opsec_session( }, ) + except ValueError: + raise HTTPException(status_code=422, detail="Invalid authorized scope") except Exception as e: logger.error(f"Error creating OPSEC session: {e}") raise HTTPException(status_code=500, detail=str(e)) diff --git a/pentestgpt/core/engagement.py b/pentestgpt/core/engagement.py index 85af769..581d4d0 100644 --- a/pentestgpt/core/engagement.py +++ b/pentestgpt/core/engagement.py @@ -4,6 +4,7 @@ import ipaddress import re +import shlex from collections.abc import Iterable, Mapping from dataclasses import dataclass from typing import TYPE_CHECKING, Any @@ -35,6 +36,10 @@ "discovered_hosts", } _CONTEXT_KEYS = {"target_info", "session_findings", "reconnaissance_data", "findings", "structured_findings"} +_COMMAND_TARGET_OPTIONS = frozenset( + {"-t", "-u", "--base-url", "--host", "--hostname", "--target", "--targets", "--url", "--urls"} +) +_POSITIONAL_TARGET_TOOLS = frozenset({"enum4linux", "enum4linux-ng", "masscan", "nmap", "nikto", "nuclei"}) def normalize_target(value: str) -> str: @@ -48,8 +53,11 @@ def normalize_target(value: str) -> str: return "" if "://" in candidate or candidate.startswith("//"): - parsed = urlsplit(candidate if "://" in candidate else f"https:{candidate}") - candidate = parsed.hostname or "" + try: + parsed = urlsplit(candidate if "://" in candidate else f"https:{candidate}") + candidate = parsed.hostname or "" + except ValueError: + return "" elif candidate.startswith("["): closing = candidate.find("]") if closing == -1: @@ -79,9 +87,9 @@ def extract_effective_targets(*contexts: Any) -> set[str]: def add_values(value: Any) -> None: if isinstance(value, str): - normalized = normalize_target(value) - if normalized: - targets.add(normalized) + candidate = value.strip() + if candidate: + targets.add(normalize_target(candidate) or candidate) return if isinstance(value, Mapping): visit_mapping(value) @@ -106,6 +114,52 @@ def visit_mapping(mapping: Mapping[Any, Any]) -> None: return targets +def extract_command_targets(command: str) -> set[str]: + """Extract explicit targets from a command before it reaches an executor. + + URL and IP literals are recognized wherever they occur. Common target + options are handled explicitly, while tools with a final positional target + use their final non-option argument. Malformed target option values are + retained so configured scopes deny them instead of silently dropping them. + """ + try: + tokens = shlex.split(command) + except ValueError: + return {command.strip()} if command.strip() else set() + if not tokens: + return set() + + targets: set[str] = set() + + def add_target(value: str) -> None: + candidate = value.strip() + if candidate: + targets.add(normalize_target(candidate) or candidate) + + for index, token in enumerate(tokens): + option, separator, value = token.partition("=") + if separator and option in _COMMAND_TARGET_OPTIONS: + add_target(value) + elif token in _COMMAND_TARGET_OPTIONS and index + 1 < len(tokens): + add_target(tokens[index + 1]) + elif "://" in token or token.startswith("//"): + add_target(token) + else: + normalized = normalize_target(token) + try: + ipaddress.ip_address(normalized) + except ValueError: + continue + targets.add(normalized) + + tool = tokens[0].rsplit("/", 1)[-1].casefold() + if tool in _POSITIONAL_TARGET_TOOLS and len(tokens) > 1: + final_token = tokens[-1] + if not final_token.startswith("-"): + add_target(final_token) + return targets + + @dataclass(frozen=True) class ScopeDecision: """Result of validating a set of targets against an engagement scope.""" @@ -126,16 +180,28 @@ class EngagementScope: roe_notes: str | None = None def __post_init__(self) -> None: - normalized_hosts = frozenset( - normalized for host in self.authorized_hosts if (normalized := normalize_target(str(host))) - ) - normalized_ranges = tuple( - dict.fromkeys( - str(ipaddress.ip_network(str(network).strip(), strict=False)) for network in self.authorized_ip_ranges - ) - ) - object.__setattr__(self, "authorized_hosts", normalized_hosts) - object.__setattr__(self, "authorized_ip_ranges", normalized_ranges) + normalized_hosts: set[str] = set() + for host in self.authorized_hosts: + if not isinstance(host, str): + raise ValueError(f"Invalid authorized host: {host!r}") + normalized = normalize_target(host) + if not normalized: + raise ValueError(f"Invalid authorized host: {host!r}") + normalized_hosts.add(normalized) + + normalized_ranges: list[str] = [] + for network in self.authorized_ip_ranges: + if not isinstance(network, str): + raise ValueError(f"Invalid authorized IP range: {network!r}") + try: + normalized_network = str(ipaddress.ip_network(network.strip(), strict=False)) + except ValueError as exc: + raise ValueError(f"Invalid authorized IP range: {network!r}") from exc + if normalized_network not in normalized_ranges: + normalized_ranges.append(normalized_network) + + object.__setattr__(self, "authorized_hosts", frozenset(normalized_hosts)) + object.__setattr__(self, "authorized_ip_ranges", tuple(normalized_ranges)) @classmethod def from_settings(cls, settings: AppSettings) -> EngagementScope | None: @@ -173,15 +239,20 @@ def is_configured(self) -> bool: def validate_targets(self, targets: set[str]) -> ScopeDecision: """Validate all effective targets, failing closed for configured scope.""" - normalized_targets = {normalized for target in targets if (normalized := normalize_target(target))} if not self.is_configured(): return ScopeDecision(True, "No engagement scope configured; authorization is not asserted") - if not normalized_targets: + if not targets: return ScopeDecision(False, "Configured engagement scope requires at least one effective target") networks = tuple(ipaddress.ip_network(network, strict=False) for network in self.authorized_ip_ranges) offending: set[str] = set() - for target in normalized_targets: + normalized_targets: set[str] = set() + for raw_target in targets: + target = normalize_target(raw_target) + if not target: + offending.add(raw_target) + continue + normalized_targets.add(target) try: address = ipaddress.ip_address(target) except ValueError: @@ -215,7 +286,10 @@ def _as_string_list(value: Any) -> list[str]: if isinstance(value, str): return [item.strip() for item in value.split(",") if item.strip()] if isinstance(value, Iterable) and not isinstance(value, (bytes, bytearray, Mapping)): - return [str(item).strip() for item in value if str(item).strip()] + items = list(value) + if not all(isinstance(item, str) for item in items): + raise ValueError("Scope hosts and IP ranges must contain only strings") + return [item.strip() for item in items if item.strip()] raise ValueError("Scope hosts and IP ranges must be strings or lists of strings") diff --git a/pentestgpt/core/scope_guard.py b/pentestgpt/core/scope_guard.py index 3b10319..461eb6a 100644 --- a/pentestgpt/core/scope_guard.py +++ b/pentestgpt/core/scope_guard.py @@ -46,11 +46,11 @@ def __init__(self, scope: EngagementScope | None, audit_sink: AuditSink) -> None def check(self, phase: str, targets: set[str]) -> ScopeDecision: """Audit and enforce a scope decision for one lifecycle phase.""" - normalized_targets = {normalized for target in targets if (normalized := normalize_target(target))} + effective_targets = {target.strip() for target in targets if target.strip()} if self.scope is None: decision = ScopeDecision(True, "No engagement scope configured; authorization is not asserted") else: - decision = self.scope.validate_targets(normalized_targets) + decision = self.scope.validate_targets(effective_targets) entry = { "timestamp": datetime.now(timezone.utc).isoformat(), @@ -59,7 +59,7 @@ def check(self, phase: str, targets: set[str]) -> ScopeDecision: "scope_configured": self.scope is not None and self.scope.is_configured(), "client": self.scope.client if self.scope else None, "engagement_id": self.scope.engagement_id if self.scope else None, - "targets": sorted(normalized_targets), + "targets": sorted(effective_targets), "offending_targets": sorted(decision.offending), "reason": decision.reason, } @@ -67,5 +67,7 @@ def check(self, phase: str, targets: set[str]) -> ScopeDecision: if not decision.allowed: raise OutOfScopeError(decision.reason, offending_targets=set(decision.offending), phase=phase) - self.last_allowed_targets = frozenset(normalized_targets) + self.last_allowed_targets = frozenset( + normalized for target in effective_targets if (normalized := normalize_target(target)) + ) return decision diff --git a/pentestgpt/orchestrator.py b/pentestgpt/orchestrator.py index 6e634dc..80b0c1a 100644 --- a/pentestgpt/orchestrator.py +++ b/pentestgpt/orchestrator.py @@ -44,7 +44,11 @@ StealthOperationType, ) from pentestgpt.config.stealth_config import StealthConfig -from pentestgpt.core.engagement import EngagementScope, extract_effective_targets +from pentestgpt.core.engagement import ( + EngagementScope, + extract_command_targets, + extract_effective_targets, +) from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard from pentestgpt.metrics_collector import MetricsCollector from pentestgpt.mitre import MITREGuidedPlanner, MITRETactic, get_technique @@ -109,6 +113,8 @@ def __init__( scope_guard: ScopeGuard | None = None, ): """Initialize PentestOrchestrator.""" + if scope_guard is not None and scope_guard.scope != engagement_scope: + raise ValueError("scope_guard scope must match engagement_scope") self.session_dir = session_dir self.engagement_scope = engagement_scope self.scope_guard = scope_guard or ScopeGuard( @@ -601,7 +607,10 @@ def _handle_repetition_check(self, next_task: PTTNode, target_info: dict[str, An async def _dispatch_executor(self, verified_commands: list[str]) -> dict[str, Any]: """Dispatch commands to the appropriate executor interface.""" - self.scope_guard.check("command_execution", self._effective_targets()) + command_targets: set[str] = set() + for command in verified_commands: + command_targets.update(extract_command_targets(command)) + self.scope_guard.check("command_execution", self._effective_targets() | command_targets) if hasattr(self.executor, "execute_batch_async") and asyncio.iscoroutinefunction( getattr(self.executor, "execute_batch_async", None) ): diff --git a/pentestgpt/prompts/prompt_class.py b/pentestgpt/prompts/prompt_class.py index 4f56241..168e362 100644 --- a/pentestgpt/prompts/prompt_class.py +++ b/pentestgpt/prompts/prompt_class.py @@ -40,8 +40,7 @@ class PentestGPTPrompt: Reply with yes if you understood.""" - input_parsing_init: str = """You're required to work as a cybersecurity penetration tester assistant. -You're an assistant for a cybersecurity penetration tester. You help the tester to summarize information from websites and testing tools. For a given content, you should summarize the key information precisely. In particular, + input_parsing_init: str = """You're an assistant for a cybersecurity penetration tester. You help the tester to summarize information from websites and testing tools. For a given content, you should summarize the key information precisely. In particular, 1. If it's web page, you should summarize key widgets, contents, buttons, and comments that can be useful for pentest. 2. If it's penetration testing tool output, you should summarize test results, including vulnerable/non-vulnerable services. 3. You should keep both finding's field name and value. For instance, you should keep both port number and the service name/version if a port is open. @@ -59,6 +58,8 @@ class PentestGPTPrompt: Risk - whether it is High, Medium or Low risk Host - The IP or the name of the Host Service - The service name that is tested + Port - The tested network port + Name - The vulnerability or finding name Description - a short description about the vulnerability Solution - a possible remediation method Vulnerability state: Whether it is present on the machine diff --git a/pentestgpt/utils/dependency_injection.py b/pentestgpt/utils/dependency_injection.py index 78601ec..80f3236 100644 --- a/pentestgpt/utils/dependency_injection.py +++ b/pentestgpt/utils/dependency_injection.py @@ -66,6 +66,15 @@ def get_task_statistics(self) -> dict[str, Any]: ... +@runtime_checkable +class EngagementScopeAware(Protocol): + """An agent that accepts the shared engagement scope and guard.""" + + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Receive the shared engagement scope state.""" + ... + + @runtime_checkable class MITREPlannerInterface(Protocol): """Interface for MITREGuidedPlanner dependencies.""" @@ -418,7 +427,7 @@ def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) def wire_engagement_scope(self, agent: T) -> T: """Inject configured scope into a compatible agent and return it.""" - if self._scope_guard is not None and hasattr(agent, "set_engagement_scope"): + if self._scope_guard is not None and isinstance(agent, EngagementScopeAware): agent.set_engagement_scope(self._engagement_scope, self._scope_guard) return agent diff --git a/pentestgpt/utils/orchestrator_factory.py b/pentestgpt/utils/orchestrator_factory.py index 12fe94e..240b247 100644 --- a/pentestgpt/utils/orchestrator_factory.py +++ b/pentestgpt/utils/orchestrator_factory.py @@ -685,6 +685,8 @@ def _configure_engagement_scope( "ScopeGuard | None", kwargs.pop("scope_guard", None), ) or ScopeGuard(scope, JsonlAuditSink(Path(session_dir) / "scope_audit.jsonl")) + if guard.scope != scope: + raise ValueError("scope_guard scope must match engagement_scope") self.agent_factory.set_engagement_scope(scope, guard) return scope, guard diff --git a/tests/agents/test_exploit_scope.py b/tests/agents/test_exploit_scope.py index 4910183..5000b89 100644 --- a/tests/agents/test_exploit_scope.py +++ b/tests/agents/test_exploit_scope.py @@ -59,6 +59,24 @@ def test_out_of_scope_exploit_generation_skips_router() -> None: router.route_task.assert_not_called() +def test_public_exploitation_workflow_propagates_scope_denial() -> None: + router = MagicMock() + router.route_task.side_effect = OutOfScopeError("scope denied") + developer = ExploitDeveloper({}, {}, model_router=router) + guard = ScopeGuard( + EngagementScope(authorized_hosts=frozenset({"lab.example.test"})), + MemoryAuditSink(), + ) + developer.set_engagement_scope(guard.scope, guard) + + with pytest.raises(OutOfScopeError): + developer.develop_and_test_exploits( + [{"vulnerability_id": "V-1", "type": "network_exploitation"}], + "lab.example.test", + "session-1", + ) + + def test_advanced_agent_forwards_scope_to_exploit_developer() -> None: agent = AdvancedExploitationAgent(MagicMock()) guard = ScopeGuard( diff --git a/tests/agents/test_opsec_agent.py b/tests/agents/test_opsec_agent.py index fb2f82b..6473f14 100644 --- a/tests/agents/test_opsec_agent.py +++ b/tests/agents/test_opsec_agent.py @@ -5,6 +5,8 @@ from typing import Any from unittest.mock import MagicMock +import pytest + from pentestgpt.agents.opsec_agent import ( EnvironmentProfile, LegalComplianceChecker, @@ -17,6 +19,7 @@ StealthConfig, StealthLevel, ) +from pentestgpt.core.exceptions import OutOfScopeError from pentestgpt.core.scope_guard import JsonlAuditSink from tests import make_mock_router @@ -307,9 +310,15 @@ def test_create_session_audit_uses_configured_session_dir(self, tmp_path: Any) - ) session_id = agent.create_session() assert agent._scope_guard is not None - sink = agent._scope_guard.audit_sink - assert isinstance(sink, JsonlAuditSink) - assert sink.path == Path(tmp_path) / session_id / "scope_audit.jsonl" + first_sink = agent._scope_guard.audit_sink + assert isinstance(first_sink, JsonlAuditSink) + assert first_sink.path == Path(tmp_path) / session_id / "scope_audit.jsonl" + + second_session_id = agent.create_session() + second_sink = agent._scope_guard.audit_sink + assert isinstance(second_sink, JsonlAuditSink) + assert second_sink.path == Path(tmp_path) / second_session_id / "scope_audit.jsonl" + assert first_sink.path == Path(tmp_path) / session_id / "scope_audit.jsonl" def test_end_current_session_clears_current(self) -> None: agent = _make_opsec_agent() @@ -362,6 +371,16 @@ def test_analyze_target_environment_error_returns_dict(self) -> None: # Error path returns error key and default opsec level assert "error" in result or "recommended_opsec_level" in result + def test_llm_scope_denials_propagate_from_opsec_entry_points(self) -> None: + router = make_mock_router(route_task_return=("Mocked response", "openai", 0.01)) + router.route_task = MagicMock(side_effect=OutOfScopeError("scope denied")) + agent = OPSECAgent(model_router=router, config=StealthConfig()) + + with pytest.raises(OutOfScopeError): + agent.analyze_target_environment({"target": "10.0.0.1"}) + with pytest.raises(OutOfScopeError): + agent.get_opsec_recommendations({"target": "10.0.0.1"}, "reconnaissance") + def test_select_stealth_level_returns_stealth_level_enum(self) -> None: agent = _make_opsec_agent() target_analysis: dict[str, Any] = { diff --git a/tests/api/test_opsec_session_scope.py b/tests/api/test_opsec_session_scope.py index 1f3e1ef..a770faf 100644 --- a/tests/api/test_opsec_session_scope.py +++ b/tests/api/test_opsec_session_scope.py @@ -2,13 +2,14 @@ from unittest.mock import MagicMock +import pytest from fastapi.testclient import TestClient from pentestgpt.api import stealth_api from pentestgpt.core.engagement import EngagementScope -def test_authorized_scope_reaches_opsec_session(monkeypatch) -> None: +def test_authorized_scope_reaches_opsec_session(monkeypatch: pytest.MonkeyPatch) -> None: opsec = MagicMock() opsec.create_session.return_value = "scope-session" monkeypatch.setattr(stealth_api, "opsec_agent", opsec) @@ -34,3 +35,18 @@ def test_authorized_scope_reaches_opsec_session(monkeypatch) -> None: assert scope.authorized_hosts == frozenset({"lab.example.test"}) assert scope.authorized_ip_ranges == ("10.40.0.0/16",) assert scope.engagement_id == "PT-10" + + +def test_malformed_authorized_scope_returns_sanitized_client_error(monkeypatch: pytest.MonkeyPatch) -> None: + opsec = MagicMock() + monkeypatch.setattr(stealth_api, "opsec_agent", opsec) + client = TestClient(stealth_api.app) + + response = client.post( + "/stealth/sessions", + json={"name": "Malformed scope", "authorized_scope": {"hosts": ["invalid/host"]}}, + ) + + assert response.status_code == 422 + assert response.json() == {"detail": "Invalid authorized scope"} + opsec.create_session.assert_not_called() diff --git a/tests/core/test_engagement.py b/tests/core/test_engagement.py index f311c23..0e1525e 100644 --- a/tests/core/test_engagement.py +++ b/tests/core/test_engagement.py @@ -1,8 +1,11 @@ """Tests for canonical engagement scope normalization and validation.""" +import pytest + from pentestgpt.config.settings import AppSettings from pentestgpt.core.engagement import ( EngagementScope, + extract_command_targets, extract_effective_targets, normalize_target, ) @@ -15,6 +18,25 @@ def test_normalize_url_hostname_ip_and_ports() -> None: assert normalize_target("192.0.2.10:22") == "192.0.2.10" +@pytest.mark.parametrize("value", ["https://[::1", "//example.test["]) +def test_normalize_target_rejects_malformed_bracketed_urls(value: str) -> None: + assert normalize_target(value) == "" + + +@pytest.mark.parametrize( + ("hosts", "ranges"), + [ + (frozenset({"invalid/host"}), ()), + (frozenset({"app.example.test"}), ("invalid-range",)), + (frozenset({1}), ()), # type: ignore[arg-type] + (frozenset(), (1,)), # type: ignore[arg-type] + ], +) +def test_scope_rejects_invalid_authorized_values(hosts: frozenset[str], ranges: tuple[str, ...]) -> None: + with pytest.raises(ValueError, match="Invalid authorized"): + EngagementScope(authorized_hosts=hosts, authorized_ip_ranges=ranges) + + def test_multi_target_validation_accepts_hosts_and_cidr_members() -> None: scope = EngagementScope( client="Example", @@ -43,6 +65,15 @@ def test_configured_scope_fails_closed_for_empty_and_mixed_targets() -> None: assert mixed.offending == frozenset({"192.0.2.50"}) +def test_configured_scope_rejects_invalid_target_alongside_authorized_target() -> None: + scope = EngagementScope(authorized_hosts=frozenset({"app.example.test"})) + + decision = scope.validate_targets({"app.example.test", "invalid/target"}) + + assert decision.allowed is False + assert decision.offending == frozenset({"invalid/target"}) + + def test_unconfigured_scope_is_neutral_and_non_authorizing() -> None: scope = EngagementScope() @@ -83,6 +114,11 @@ def test_extract_effective_targets_includes_primary_and_discovered_hosts() -> No assert targets == {"app.example.test", "10.0.0.8", "db.example.test"} +def test_extract_command_targets_finds_known_tool_positional_and_option_targets() -> None: + assert extract_command_targets("nmap -sV app.example.test") == {"app.example.test"} + assert extract_command_targets("curl --url https://192.0.2.25/status") == {"192.0.2.25"} + + def test_to_opsec_context_uses_normalized_canonical_scope() -> None: scope = EngagementScope( authorized_hosts=frozenset({"App.Example.Test"}), diff --git a/tests/integration/test_scope_guard_lifecycle.py b/tests/integration/test_scope_guard_lifecycle.py index 9300201..8829b01 100644 --- a/tests/integration/test_scope_guard_lifecycle.py +++ b/tests/integration/test_scope_guard_lifecycle.py @@ -53,6 +53,11 @@ async def test_scope_guard_blocks_discovered_out_of_scope_host_before_llm_and_ex assert "AUTHORIZED ENGAGEMENT CONTEXT" in system_prompt assert "10.0.0.5" in system_prompt + with pytest.raises(OutOfScopeError): + await orchestrator._dispatch_executor(["nmap 192.0.2.25"]) + executor.execute_batch_async.assert_not_called() + executor.execute_batch.assert_not_called() + orchestrator._session_findings["discovered_hosts"].append("192.0.2.25") router.reset_mock() @@ -68,3 +73,25 @@ async def test_scope_guard_blocks_discovered_out_of_scope_host_before_llm_and_ex entries = [json.loads(line) for line in audit_path.read_text(encoding="utf-8").splitlines()] assert any(entry["decision"] == "allow" for entry in entries) assert any(entry["decision"] == "deny" and entry["offending_targets"] == ["192.0.2.25"] for entry in entries) + + +def test_direct_orchestrator_rejects_mismatched_scope_guard(tmp_path: Path) -> None: + scope = EngagementScope(authorized_hosts=frozenset({"app.example.test"})) + guard = ScopeGuard( + EngagementScope(authorized_hosts=frozenset({"other.example.test"})), + JsonlAuditSink(tmp_path / "scope_audit.jsonl"), + ) + + with pytest.raises(ValueError, match="scope_guard scope must match engagement_scope"): + PentestOrchestrator( + model_router=MagicMock(), + strategy_agent=MagicMock(), + command_generator=MagicMock(), + repetition_detector=MagicMock(), + results_verifier=MagicMock(), + tool_executor=MagicMock(), + mitre_planner=MagicMock(), + session_dir=str(tmp_path), + engagement_scope=scope, + scope_guard=guard, + ) diff --git a/tests/prompts/test_prompt_templates.py b/tests/prompts/test_prompt_templates.py index f85fff5..e401d5d 100644 --- a/tests/prompts/test_prompt_templates.py +++ b/tests/prompts/test_prompt_templates.py @@ -241,7 +241,7 @@ def test_mitre_system_prompt_no_placeholders(self) -> None: # into the static templates). # --------------------------------------------------------------------------- -UNCONDITIONAL_AUTHORIZATION_PHRASES = [ +UNCONDITIONAL_AUTHORIZATION_PHRASES: list[str] = [ "obtained valid certificates", "valid certificates and approval", "valid permission to perform", @@ -265,9 +265,11 @@ class TestNoUnconditionalAuthorizationClaims: def test_dataclass_prompts_make_no_authorization_claim(self, cls: type) -> None: instance = cls() for f in dataclasses.fields(instance): - value = getattr(instance, f.name).lower() - for phrase in UNCONDITIONAL_AUTHORIZATION_PHRASES: - assert phrase not in value, f"{cls.__name__}.{f.name} asserts authorization: {phrase!r}" + value = getattr(instance, f.name) + if isinstance(value, str): + value_lower = value.lower() + for phrase in UNCONDITIONAL_AUTHORIZATION_PHRASES: + assert phrase not in value_lower, f"{cls.__name__}.{f.name} asserts authorization: {phrase!r}" # --------------------------------------------------------------------------- diff --git a/tests/utils/test_orchestrator_factory.py b/tests/utils/test_orchestrator_factory.py index 1d67e39..2801c48 100644 --- a/tests/utils/test_orchestrator_factory.py +++ b/tests/utils/test_orchestrator_factory.py @@ -20,6 +20,7 @@ from pentestgpt.config.settings import AppSettings from pentestgpt.core.engagement import EngagementScope +from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard from pentestgpt.orchestrator import PentestOrchestrator from pentestgpt.utils.dependency_injection import ( DependencyContainer, @@ -243,6 +244,28 @@ def test_engagement_scope_is_shared_by_factory_agents( assert scoped_agent._engagement_scope is scope assert scoped_agent._scope_guard is orchestrator.scope_guard + def test_rejects_scope_guard_with_different_scope( + self, + mock_router: MagicMock, + mock_mitre_planner: MagicMock, + all_flags_off: AppSettings, + tmp_path: Any, + ) -> None: + configured_scope = EngagementScope(authorized_hosts=frozenset({"app.example.test"})) + guard = ScopeGuard( + EngagementScope(authorized_hosts=frozenset({"other.example.test"})), + JsonlAuditSink(tmp_path / "scope_audit.jsonl"), + ) + + with pytest.raises(ValueError, match="scope_guard scope must match engagement_scope"): + OrchestratorFactory().create_orchestrator( + model_router=mock_router, + mitre_planner=mock_mitre_planner, + session_dir=str(tmp_path), + engagement_scope=configured_scope, + scope_guard=guard, + ) + def test_rag_system_registered_and_forwarded( self, mock_router: MagicMock, From 1d489d1af847330c85d1a35635e9eb3ae962ad5e Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:27:34 +0800 Subject: [PATCH 07/12] chore: ignore generated artifacts --- .gitignore | 4 + bandit_results.json | 3699 ------------------------------------------- 2 files changed, 4 insertions(+), 3699 deletions(-) delete mode 100644 bandit_results.json diff --git a/.gitignore b/.gitignore index 97dd850..1c650bd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,12 @@ __pycache__/ *$py.class config/chatgpt_config.py outputs/ +output/ .idea .codeboarding/ .kilo/ +.qodo/ +node_modules/ SOUL.md logs/ utils/logs/ @@ -208,6 +211,7 @@ pytest_full_output.txt final_mypy_results.txt final_final_mypy_results.txt coverage.json +bandit_results.json # Generated reports reports/ diff --git a/bandit_results.json b/bandit_results.json deleted file mode 100644 index 2daf886..0000000 --- a/bandit_results.json +++ /dev/null @@ -1,3699 +0,0 @@ -{ - "errors": [], - "generated_at": "2026-03-13T13:29:56Z", - "metrics": { - "Survey_analysis/analysis.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 47, - "nosec": 0, - "skipped_tests": 0 - }, - "_totals": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 123638, - "nosec": 164, - "skipped_tests": 3 - }, - "pentestgpt/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 14, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/_version.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 74, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/advanced_agent_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 135, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/advanced_agent_manager_refactored.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 32, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/advanced_exploitation_agent.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 251, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/agent_abstract.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 377, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/command_generator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 360, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 0, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/agent_lifecycle_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 173, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/agent_scaler.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 197, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/attack_path_generator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 264, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/attack_surface_analyzer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 199, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/communication_hub.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 252, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/health_monitor.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 170, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/load_balancer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 222, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/mitre_integrator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 237, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/performance_monitor.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 168, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/strategic_planner.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 145, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/system_integrator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 153, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/task_orchestrator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 194, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/task_prioritizer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 111, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/components/technique_advisor.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 213, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/exploit_developer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 335, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/intelligence_gathering_agent.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 831, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/knowledge_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 435, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/machine_learning_agent.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 81, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/monitoring_reporter.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 218, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/network_operations_agent.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1534, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/opsec_agent.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 613, - "nosec": 2, - "skipped_tests": 0 - }, - "pentestgpt/agents/post_exploitation_handler.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 137, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/agents/repetition_detector.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 322, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/results_verifier.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 360, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/session_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 323, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/stealth_operations_agent.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1028, - "nosec": 2, - "skipped_tests": 0 - }, - "pentestgpt/agents/strategy_agent.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 276, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/strategy_agent_refactored.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 31, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/stub_tool_executor.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 48, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/agents/tool_executor.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 773, - "nosec": 14, - "skipped_tests": 0 - }, - "pentestgpt/agents/vulnerability_scanner.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 119, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/api/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 3, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/api/agent_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 291, - "nosec": 3, - "skipped_tests": 0 - }, - "pentestgpt/api/cost_optimization_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 749, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/api/detection_resistance_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1064, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/api/effectiveness_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 642, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/api/knowledge_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 693, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/api/middleware/auth_middleware.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 203, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/api/monitoring_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 785, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/api/monitoring_api_secure.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 328, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/api/network_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 775, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/api/plugin_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 771, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/api/stealth_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 683, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/api/testing_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 843, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/benchmark.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 82, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/communication/agent_communication.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 827, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 0, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/agent_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 835, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/auth_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 99, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/chatgpt_config_sample.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 16, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/config_examples.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 529, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/cost_optimization_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 640, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/config/detection_resistance_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 676, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/effectiveness_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1238, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/knowledge_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 688, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/llm_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 173, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/monitoring_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1153, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/network_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 619, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/plugin_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 233, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/stealth_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 421, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/config/testing_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 354, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/core/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 32, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/core/app_context.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 116, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/core/exceptions.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 155, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/cost_optimization/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 17, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/cost_optimization/cost_intelligence_analyzer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 776, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/cost_optimization/intelligent_budget_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 602, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/cost_optimization/performance_optimization_engine.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 850, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/cost_optimization/quality_assurance_system.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 847, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/cost_optimization/reliability_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 674, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/cost_optimization/system_resilience_framework.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 951, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/data/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 12, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/data/models/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 8, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/data/models/base.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 216, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/data/repositories/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 8, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/data/repositories/base_repository.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 360, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/data/services/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 8, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/data/services/data_service.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 378, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 21, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/agent_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1093, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/cost_optimization_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1064, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/detection_resistance_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1002, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/effectiveness_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 749, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/knowledge_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 699, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/monitoring_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 590, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/network_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1066, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/plugin_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 395, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/stealth_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 777, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/database/testing_models.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 980, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/detection_resistance/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 50, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/detection_resistance/ai_ml_resistance.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 2364, - "nosec": 21, - "skipped_tests": 0 - }, - "pentestgpt/detection_resistance/behavioral_resistance.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1003, - "nosec": 16, - "skipped_tests": 0 - }, - "pentestgpt/detection_resistance/detection_assessment_engine.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1937, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/detection_resistance/edr_evasion.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1399, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/detection_resistance/network_analysis_resistance.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1896, - "nosec": 11, - "skipped_tests": 0 - }, - "pentestgpt/detection_resistance/signature_resistance.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 851, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/effectiveness/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 19, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/effectiveness/attack_strategy_optimizer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1003, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/effectiveness/effectiveness_orchestrator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1083, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/effectiveness/enterprise_environment_handler.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1438, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/effectiveness/success_rate_analytics.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1362, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/effectiveness/target_environment_analyzer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 854, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/effectiveness/validation_confirmation_system.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1296, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/extract_cookie.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 36, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/knowledge/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/knowledge/adaptive_technique_selector.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 554, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/knowledge/dynamic_knowledge_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 688, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/knowledge/intelligence_feed_integrator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 883, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/knowledge/knowledge_evolution_system.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 959, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/knowledge/learning_engine.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1047, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llm_integration/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 13, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llm_integration/base_provider.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 255, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llm_integration/llm_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 235, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llm_integration/providers/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 10, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llm_integration/providers/anthropic_provider.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 276, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llm_integration/providers/openai_provider.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 311, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llms/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 0, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llms/anthropic.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 105, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llms/ollama.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 105, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/llms/openai.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 116, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llms/router.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 93, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/llms/xai.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 92, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/main.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 534, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/mitre.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 292, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/monitoring/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/monitoring/alert_correlation.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 995, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/monitoring/detection_intelligence.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1133, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/monitoring/detection_monitor.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 971, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/monitoring/response_controller.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1316, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/monitoring/security_event_analyzer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1400, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/network/protocol_evasion.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 882, - "nosec": 3, - "skipped_tests": 0 - }, - "pentestgpt/network/segmentation_traversal.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 844, - "nosec": 3, - "skipped_tests": 0 - }, - "pentestgpt/network/traffic_obfuscation_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 629, - "nosec": 2, - "skipped_tests": 0 - }, - "pentestgpt/orchestrator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 406, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugin.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 287, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 9, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/core/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 12, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/core/base_plugin.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 134, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/core/plugin_interface.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 176, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/core/plugin_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 216, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/development/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 12, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/development/plugin_development_kit.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 348, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/development/plugin_template.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 466, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/development/plugin_validator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 563, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/examples/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 4, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/examples/network_scanner/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 5, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/examples/network_scanner/plugin.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 609, - "nosec": 7, - "skipped_tests": 0 - }, - "pentestgpt/plugins/plugin_api.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 116, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/plugin_development_kit.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1374, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/plugins/plugin_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 674, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/plugin_marketplace.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 606, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/plugin_rating.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 35, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/plugin_registry.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 636, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/plugins/plugin_security.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 766, - "nosec": 2, - "skipped_tests": 0 - }, - "pentestgpt/prompts/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 0, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/prompts/mitre_prompts.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 568, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/prompts/prompt_class.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 138, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/prompts/prompt_class_v1.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 48, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/prompts/prompt_class_v2.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 109, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/prompts/prompt_class_v3.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 365, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/security/compliance_monitor.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 883, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/security/dependency_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 312, - "nosec": 3, - "skipped_tests": 0 - }, - "pentestgpt/security/security_auditor.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 718, - "nosec": 8, - "skipped_tests": 0 - }, - "pentestgpt/security/update_scheduler.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 627, - "nosec": 7, - "skipped_tests": 0 - }, - "pentestgpt/security/vulnerability_tracker.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 593, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 16, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/base_analyzer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 269, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/config/secure_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 277, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/pattern_matching_engine.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 684, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/recommendation_generator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 853, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/security/authentication.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 297, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/statistical_analysis_engine.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 775, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/thread_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 700, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/shared/utils/security.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 329, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/tasks/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 0, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/tasks/crawler.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 134, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/tasks/example_sqlmap.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 15, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/tasks/test_os_execution.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 16, - "nosec": 1, - "skipped_tests": 1 - }, - "pentestgpt/test_config_compatibility.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 104, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/test_connection.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 469, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/testing/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 8, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/testing/automation_testing_framework.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1439, - "nosec": 13, - "skipped_tests": 0 - }, - "pentestgpt/testing/integration_testing_suite.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1041, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/testing/performance_benchmarking.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 905, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/testing/production_readiness_validator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1211, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/testing/security/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 8, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/testing/security/fixtures.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 182, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/testing/security/test_security_vulnerabilities.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 231, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/testing/security_validation_suite.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1151, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/testing/validation_framework.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 687, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/ui/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/ui/tui.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 77, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/APIs/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 0, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/APIs/anthropic_provider.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 215, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/APIs/base_provider.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 216, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/APIs/ollama_provider.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 258, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/APIs/openai_provider.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 231, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/APIs/xai_provider.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 189, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 9, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/advanced_agent_orchestrator_integration.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 225, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/config_factory.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 243, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/config_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 75, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/config_validation.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 486, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/cost_optimization_orchestrator_integration.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 155, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/dependency_injection.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 384, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/detection_resistance_integration_validation.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 968, - "nosec": 1, - "skipped_tests": 0 - }, - "pentestgpt/utils/detection_resistance_orchestrator_integration.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1096, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/effectiveness_orchestrator_integration.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1031, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/enhanced_rag_system.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 3, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/error_handler.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 249, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/evasion_engine.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 646, - "nosec": 7, - "skipped_tests": 0 - }, - "pentestgpt/utils/exceptions.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 37, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/helpers/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 12, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/helpers/file_helper.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 432, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/helpers/network_helper.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 350, - "nosec": 7, - "skipped_tests": 0 - }, - "pentestgpt/utils/helpers/string_helper.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 282, - "nosec": 2, - "skipped_tests": 0 - }, - "pentestgpt/utils/mitre_attack.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 909, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/mitre_kb.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 432, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/mitre_planner.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 594, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/model_router.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 423, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/network_traffic_analyzer.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1037, - "nosec": 2, - "skipped_tests": 0 - }, - "pentestgpt/utils/orchestrator_factory.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 212, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/performance_benchmark.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 390, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/plugin_orchestrator_integration.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 375, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/prompt_select.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 70, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/ptt_manager.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 418, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/rag.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 380, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/simplified_config.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 631, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/spinner.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 33, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/stealth_integration_guide.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 307, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/stealth_orchestrator_integration.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 740, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/strategy_adaptation_controller.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 668, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/task_handler.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 102, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/telemetry.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 86, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/testing_orchestrator_integration.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 1027, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/validation/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 12, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/validation/config_validator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 229, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/validation/data_validator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 305, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/utils/validation/input_validator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 346, - "nosec": 4, - "skipped_tests": 0 - }, - "pentestgpt/validation/__init__.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 16, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/validation/error_checker.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 631, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/validation/import_validation.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 61, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/validation/improvement_validator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 299, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/validation/integration_tester.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 712, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/validation/performance_validator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 730, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/validation/run_comprehensive_validation.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 205, - "nosec": 0, - "skipped_tests": 0 - }, - "pentestgpt/validation/security_validator.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 895, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/MIGRATION_SCRIPT.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 485, - "nosec": 6, - "skipped_tests": 0 - }, - "scripts/Tools_attach.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 226, - "nosec": 1, - "skipped_tests": 2 - }, - "scripts/add_missing_imports.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 74, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/comprehensive_syntax_checker.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 216, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/deep_import_checker.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 159, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/final_import_validation.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 158, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/find_import_errors.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 214, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/fix_unused_ignores.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 76, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/focused_syntax_checker.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 181, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/import_error_discovery.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 452, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/improved_syntax_checker.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 273, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/metasploit_tool_check.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 33, - "nosec": 1, - "skipped_tests": 0 - }, - "scripts/remove_commented_code.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 31, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/repitition_identifier.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 60, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/schedule.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 19, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/simple_security_test.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 66, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/simplify_expressions.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 30, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/syntax_checker.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 256, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/targeted_syntax_checker.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 213, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/validate_cost_optimization.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 423, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/validate_integration.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 375, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/validate_system.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 536, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/validate_v2.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 408, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/validate_v3_reorg.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 138, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/verify_specific_imports.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 37, - "nosec": 0, - "skipped_tests": 0 - }, - "scripts/verify_upgrade.py": { - "CONFIDENCE.HIGH": 0, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 0, - "SEVERITY.MEDIUM": 0, - "SEVERITY.UNDEFINED": 0, - "loc": 85, - "nosec": 0, - "skipped_tests": 0 - } - }, - "results": [] -} From 0f1cba4ae34d8a71ddb715bdd554c6b1d780ccdd Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:47:45 +0800 Subject: [PATCH 08/12] fix: harden engagement scope enforcement --- .coderabbit.yaml | 6 - pentestgpt/agents/command_generator.py | 3 + pentestgpt/agents/results_verifier.py | 20 +- pentestgpt/agents/strategy_agent.py | 11 + pentestgpt/core/engagement.py | 242 +++++++++++++++--- pentestgpt/core/scope_guard.py | 8 +- pentestgpt/llms/router.py | 15 +- pentestgpt/prompts/authorization.py | 7 +- tests/agents/test_results_verifier.py | 29 +++ tests/core/test_engagement.py | 33 +++ tests/core/test_scope_guard.py | 9 + .../integration/test_scope_guard_lifecycle.py | 5 + tests/llms/test_router_cache_isolation.py | 56 ++++ tests/prompts/test_authorization_preamble.py | 18 ++ 14 files changed, 409 insertions(+), 53 deletions(-) delete mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml deleted file mode 100644 index 599a16b..0000000 --- a/.coderabbit.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json -# TEMPORARY (PR #15): exclude docs/ so CodeRabbit stays under its per-PR file -# limit. Revert this file once the review is complete. -reviews: - path_filters: - - "!docs/**" diff --git a/pentestgpt/agents/command_generator.py b/pentestgpt/agents/command_generator.py index 4624359..cd85c4a 100644 --- a/pentestgpt/agents/command_generator.py +++ b/pentestgpt/agents/command_generator.py @@ -13,6 +13,7 @@ from loguru import logger from pentestgpt.agents.parsers.registry import ParserRegistry +from pentestgpt.core.exceptions import OutOfScopeError from pentestgpt.llms.router import ModelRouter, TaskType from pentestgpt.mitre import MITRETechnique from pentestgpt.prompts.authorization import MITRE_PENTEST_ROLE @@ -598,6 +599,8 @@ def refine_command( ) return str(corrected) + except OutOfScopeError: + raise except Exception as e: logger.error(f"Failed to refine command: {e}") return command diff --git a/pentestgpt/agents/results_verifier.py b/pentestgpt/agents/results_verifier.py index 76a8646..faffc8b 100644 --- a/pentestgpt/agents/results_verifier.py +++ b/pentestgpt/agents/results_verifier.py @@ -14,6 +14,8 @@ from loguru import logger from pentestgpt.agents.parsers.registry import ParserRegistry +from pentestgpt.core.engagement import extract_command_targets +from pentestgpt.core.exceptions import OutOfScopeError from pentestgpt.llms.router import ModelRouter, TaskType from pentestgpt.mitre import MITRETechnique from pentestgpt.prompts.authorization import ( @@ -61,6 +63,11 @@ def get_health_status(self) -> dict[str, Any]: """Get agent health status.""" return {"status": "healthy", "is_healthy": True} + @staticmethod + def _command_targets(commands: list[str]) -> set[str]: + """Extract effective targets without treating commands as host strings.""" + return {target for command in commands for target in extract_command_targets(command)} + def verify_commands( self, commands: list[str], @@ -103,9 +110,11 @@ def verify_commands( system_prompt=self._build_system_prompt( MITRE_PENTEST_ROLE, "command_verification", - commands, + {"targets": self._command_targets(commands)}, ), ) + except OutOfScopeError: + raise except (TimeoutError, FuturesTimeoutError): # Timeouts should propagate so callers/tests can handle them explicitly. raise @@ -259,8 +268,7 @@ def analyze_results( system_prompt=self._build_system_prompt( MITRE_PENTEST_ROLE, "result_analysis", - commands, - results, + {"targets": self._command_targets(commands)}, ), ) @@ -288,6 +296,8 @@ def analyze_results( return analysis + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error analyzing results: {e}") return { @@ -557,6 +567,8 @@ def suggest_fixes( logger.info(f"Generated {len(fixes)} fix suggestions (cost: ${cost:.4f})") return fixes + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error suggesting fixes: {e}") return [] @@ -605,6 +617,8 @@ def parse_tool_output( logger.info(f"Output parsed (cost: ${cost:.4f}, provider: {provider})") return parsed + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error parsing output: {e}") return {"raw_output": output, "error": str(e)} diff --git a/pentestgpt/agents/strategy_agent.py b/pentestgpt/agents/strategy_agent.py index 570596a..2dc6e3d 100644 --- a/pentestgpt/agents/strategy_agent.py +++ b/pentestgpt/agents/strategy_agent.py @@ -13,6 +13,7 @@ from loguru import logger +from pentestgpt.core.exceptions import OutOfScopeError from pentestgpt.mitre import MITREGuidedPlanner, MITRETactic, get_technique from pentestgpt.mitre.taxonomy import MITRE_TECHNIQUES from pentestgpt.prompts.authorization import MITRE_PENTEST_ROLE @@ -202,6 +203,8 @@ def update_ptt_with_findings(self, ptt_state: str, findings: dict[str, Any]) -> logger.info(f"PTT updated (cost: ${cost:.4f}, provider: {provider})") return response + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error updating PTT: {e}") return ptt_state # Return original if update fails @@ -281,6 +284,8 @@ def prioritize_tasks( logger.info(f"Tasks prioritized (cost: ${cost:.4f}, provider: {provider})") return prioritized + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error prioritizing tasks: {e}") return tasks # Return original order if prioritization fails @@ -344,6 +349,8 @@ def suggest_alternative_technique( ) return alt_id + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error suggesting alternative: {e}") return None @@ -390,6 +397,8 @@ def assess_attack_surface(self, target_info: dict[str, Any]) -> dict[str, Any]: ) return assessment + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error assessing attack surface: {e}") return {"error": str(e), "entry_points": [], "risk_level": "unknown"} @@ -447,6 +456,8 @@ def generate_attack_path( ) return attack_path + except OutOfScopeError: + raise except Exception as e: logger.error(f"Error generating attack path: {e}") return [] diff --git a/pentestgpt/core/engagement.py b/pentestgpt/core/engagement.py index 581d4d0..36e19c9 100644 --- a/pentestgpt/core/engagement.py +++ b/pentestgpt/core/engagement.py @@ -18,6 +18,7 @@ r"^(?=.{1,253}\.?$)(?:[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?\.)*" r"[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?\.?$", re.IGNORECASE, ) +_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=") _TARGET_KEYS = { "target", "targets", @@ -36,10 +37,108 @@ "discovered_hosts", } _CONTEXT_KEYS = {"target_info", "session_findings", "reconnaissance_data", "findings", "structured_findings"} -_COMMAND_TARGET_OPTIONS = frozenset( - {"-t", "-u", "--base-url", "--host", "--hostname", "--target", "--targets", "--url", "--urls"} +_COMMON_COMMAND_TARGET_OPTIONS = frozenset( + { + "--base-url", + "--dc-ip", + "--domain", + "--host", + "--hostname", + "--ip", + "--rhost", + "--rhosts", + "--target", + "--target-ip", + "--targets", + "--url", + "--urls", + } +) +_TOOL_COMMAND_TARGET_OPTIONS: dict[str, frozenset[str]] = { + "amass": frozenset({"-d"}), + "dnsrecon": frozenset({"-d"}), + "feroxbuster": frozenset({"-u"}), + "ffuf": frozenset({"-u"}), + "fierce": frozenset({"--domain"}), + "gobuster": frozenset({"-u"}), + "nikto": frozenset({"-h"}), + "nuclei": frozenset({"-u"}), + "sqlmap": frozenset({"-u"}), + "sublist3r": frozenset({"-d"}), + "theharvester": frozenset({"-d"}), + "wpscan": frozenset({"--url"}), + "bloodhound-python": frozenset({"-dc"}), + "certipy": frozenset({"-dc-ip", "-target"}), + "certipy-ad": frozenset({"-dc-ip", "-target"}), + "evil-winrm": frozenset({"-i"}), +} +_TOOL_COMMAND_VALUE_OPTIONS: dict[str, frozenset[str]] = { + "curl": frozenset( + { + "-A", + "-H", + "-X", + "-d", + "-e", + "-o", + "-u", + "--data", + "--data-binary", + "--header", + "--output", + "--referer", + "--request", + "--user", + "--user-agent", + } + ), + "ffuf": frozenset({"-H", "-d", "-maxtime", "-o", "-of", "-p", "-t", "-w"}), + "masscan": frozenset({"-oB", "-oG", "-oJ", "-oL", "-oX", "-p", "--ports", "--rate"}), + "hydra": frozenset({"-L", "-P", "-l", "-p", "-s", "-t", "-w"}), + "nxc": frozenset({"-H", "-d", "-p", "-u", "--domain", "--hash", "--password", "--port", "--username"}), + "netexec": frozenset({"-H", "-d", "-p", "-u", "--domain", "--hash", "--password", "--port", "--username"}), + "crackmapexec": frozenset({"-H", "-d", "-p", "-u", "--domain", "--hash", "--password", "--port", "--username"}), + "cme": frozenset({"-H", "-d", "-p", "-u", "--domain", "--hash", "--password", "--port", "--username"}), + "nmap": frozenset( + { + "-D", + "-S", + "-e", + "-g", + "-oA", + "-oG", + "-oN", + "-oS", + "-oX", + "-p", + "--data", + "--data-length", + "--data-string", + "--host-timeout", + "--max-rate", + "--max-retries", + "--min-rate", + "--proxies", + "--scan-delay", + "--script", + "--script-args", + "--source-port", + "--spoof-mac", + "--stylesheet", + "--top-ports", + "--ttl", + } + ), + "nuclei": frozenset({"-H", "-dast-server", "-o", "-rl", "-t", "-w", "--templates", "--workflows"}), +} +_POSITIONAL_ALL_TARGET_TOOLS = frozenset({"masscan", "nmap", "nuclei"}) +_POSITIONAL_FIRST_TARGET_TOOLS = frozenset({"curl", "dirb", "hydra"}) +_POSITIONAL_SECOND_TARGET_TOOLS = frozenset({"cme", "crackmapexec", "netexec", "nxc"}) +_POSITIONAL_LAST_TARGET_TOOLS = frozenset({"dnsenum", "enum4linux", "enum4linux-ng", "hping3"}) +_COMMAND_WRAPPERS_WITH_ARGS: dict[str, int] = {"stdbuf": 1, "timeout": 1} +_COMMAND_WRAPPERS_WITHOUT_ARGS = frozenset( + {"env", "ionice", "ltrace", "nice", "nohup", "setsid", "strace", "sudo", "unbuffer"} ) -_POSITIONAL_TARGET_TOOLS = frozenset({"enum4linux", "enum4linux-ng", "masscan", "nmap", "nikto", "nuclei"}) def normalize_target(value: str) -> str: @@ -81,6 +180,17 @@ def normalize_target(value: str) -> str: return candidate if _HOSTNAME_RE.fullmatch(candidate) else "" +def normalize_scope_target(value: str) -> str: + """Normalize one effective host, IP literal, URL, or CIDR target.""" + normalized = normalize_target(value) + if normalized: + return normalized + try: + return str(ipaddress.ip_network(value.strip(), strict=False)) + except ValueError: + return "" + + def extract_effective_targets(*contexts: Any) -> set[str]: """Extract explicit target fields from nested engagement inputs.""" targets: set[str] = set() @@ -89,7 +199,7 @@ def add_values(value: Any) -> None: if isinstance(value, str): candidate = value.strip() if candidate: - targets.add(normalize_target(candidate) or candidate) + targets.add(normalize_scope_target(candidate) or candidate) return if isinstance(value, Mapping): visit_mapping(value) @@ -117,10 +227,10 @@ def visit_mapping(mapping: Mapping[Any, Any]) -> None: def extract_command_targets(command: str) -> set[str]: """Extract explicit targets from a command before it reaches an executor. - URL and IP literals are recognized wherever they occur. Common target - options are handled explicitly, while tools with a final positional target - use their final non-option argument. Malformed target option values are - retained so configured scopes deny them instead of silently dropping them. + URL, IP, and CIDR literals are recognized wherever they occur. Target and + non-target options are interpreted per tool so credentials, thread counts, + output paths, and template names are not mistaken for hosts. Malformed + target values are retained so configured scopes deny them. """ try: tokens = shlex.split(command) @@ -134,32 +244,77 @@ def extract_command_targets(command: str) -> set[str]: def add_target(value: str) -> None: candidate = value.strip() if candidate: - targets.add(normalize_target(candidate) or candidate) - - for index, token in enumerate(tokens): + targets.add(normalize_scope_target(candidate) or candidate) + + tool, tool_index = _extract_command_tool(tokens) + target_options = _COMMON_COMMAND_TARGET_OPTIONS | _TOOL_COMMAND_TARGET_OPTIONS.get(tool, frozenset()) + value_options = _TOOL_COMMAND_VALUE_OPTIONS.get(tool, frozenset()) + positional: list[str] = [] + index = tool_index + 1 + while index < len(tokens): + token = tokens[index] option, separator, value = token.partition("=") - if separator and option in _COMMAND_TARGET_OPTIONS: - add_target(value) - elif token in _COMMAND_TARGET_OPTIONS and index + 1 < len(tokens): - add_target(tokens[index + 1]) - elif "://" in token or token.startswith("//"): - add_target(token) + if option in value_options: + if not separator and index + 1 < len(tokens): + index += 1 + elif option in target_options: + if separator: + add_target(value) + elif index + 1 < len(tokens): + add_target(tokens[index + 1]) + index += 1 + elif token.startswith("-"): + pass else: - normalized = normalize_target(token) - try: - ipaddress.ip_address(normalized) - except ValueError: - continue - targets.add(normalized) - - tool = tokens[0].rsplit("/", 1)[-1].casefold() - if tool in _POSITIONAL_TARGET_TOOLS and len(tokens) > 1: - final_token = tokens[-1] - if not final_token.startswith("-"): - add_target(final_token) + normalized = normalize_scope_target(token) + if "://" in token or token.startswith("//") or _is_ip_target(normalized): + add_target(token) + elif "@" in token: + credential_target = token.rsplit("@", 1)[1] + if normalize_scope_target(credential_target): + add_target(credential_target) + positional.append(token) + index += 1 + + if tool in _POSITIONAL_ALL_TARGET_TOOLS: + for candidate in positional: + add_target(candidate) + elif tool in _POSITIONAL_FIRST_TARGET_TOOLS and positional: + add_target(positional[0]) + elif tool in _POSITIONAL_SECOND_TARGET_TOOLS and len(positional) > 1: + add_target(positional[1]) + elif tool in _POSITIONAL_LAST_TARGET_TOOLS and positional: + add_target(positional[-1]) return targets +def _extract_command_tool(tokens: list[str]) -> tuple[str, int]: + """Return the unwrapped command name and its token index.""" + index = 0 + while index < len(tokens): + token = tokens[index] + if _ENV_ASSIGNMENT_RE.match(token): + index += 1 + continue + if token in _COMMAND_WRAPPERS_WITHOUT_ARGS: + index += 1 + continue + if token in _COMMAND_WRAPPERS_WITH_ARGS: + index += 1 + _COMMAND_WRAPPERS_WITH_ARGS[token] + continue + return token.rsplit("/", 1)[-1].casefold(), index + return "", len(tokens) + + +def _is_ip_target(value: str) -> bool: + """Return whether a normalized scope target is an IP or IP network.""" + try: + ipaddress.ip_network(value, strict=False) + except ValueError: + return False + return True + + @dataclass(frozen=True) class ScopeDecision: """Result of validating a set of targets against an engagement scope.""" @@ -246,13 +401,34 @@ def validate_targets(self, targets: set[str]) -> ScopeDecision: networks = tuple(ipaddress.ip_network(network, strict=False) for network in self.authorized_ip_ranges) offending: set[str] = set() - normalized_targets: set[str] = set() for raw_target in targets: target = normalize_target(raw_target) if not target: - offending.add(raw_target) + try: + target_network = ipaddress.ip_network(raw_target.strip(), strict=False) + except ValueError: + offending.add(raw_target) + continue + exact_host_authorized = ( + target_network.num_addresses == 1 + and str(target_network.network_address).casefold() in self.authorized_hosts + ) + contained_by_authorized_range = any( + ( + isinstance(target_network, ipaddress.IPv4Network) + and isinstance(network, ipaddress.IPv4Network) + and target_network.subnet_of(network) + ) + or ( + isinstance(target_network, ipaddress.IPv6Network) + and isinstance(network, ipaddress.IPv6Network) + and target_network.subnet_of(network) + ) + for network in networks + ) + if not exact_host_authorized and not contained_by_authorized_range: + offending.add(str(target_network)) continue - normalized_targets.add(target) try: address = ipaddress.ip_address(target) except ValueError: @@ -269,7 +445,7 @@ def validate_targets(self, targets: set[str]) -> ScopeDecision: def to_opsec_context(self, targets: list[str]) -> dict[str, Any]: """Adapt this scope to the context consumed by LegalComplianceChecker.""" - normalized_targets = sorted(normalized for target in targets if (normalized := normalize_target(target))) + normalized_targets = sorted(normalized for target in targets if (normalized := normalize_scope_target(target))) return { "authorized": self.is_configured(), "scope": { diff --git a/pentestgpt/core/scope_guard.py b/pentestgpt/core/scope_guard.py index 461eb6a..261cac5 100644 --- a/pentestgpt/core/scope_guard.py +++ b/pentestgpt/core/scope_guard.py @@ -8,7 +8,11 @@ from threading import Lock from typing import Any, Protocol -from pentestgpt.core.engagement import EngagementScope, ScopeDecision, normalize_target +from pentestgpt.core.engagement import ( + EngagementScope, + ScopeDecision, + normalize_scope_target, +) from pentestgpt.core.exceptions import OutOfScopeError @@ -68,6 +72,6 @@ def check(self, phase: str, targets: set[str]) -> ScopeDecision: if not decision.allowed: raise OutOfScopeError(decision.reason, offending_targets=set(decision.offending), phase=phase) self.last_allowed_targets = frozenset( - normalized for target in effective_targets if (normalized := normalize_target(target)) + normalized for target in effective_targets if (normalized := normalize_scope_target(target)) ) return decision diff --git a/pentestgpt/llms/router.py b/pentestgpt/llms/router.py index f76a94f..3a377d9 100644 --- a/pentestgpt/llms/router.py +++ b/pentestgpt/llms/router.py @@ -309,16 +309,18 @@ def route_task( """ start_time = time.time() - # Isolate cached responses by both user message and system-prompt identity. - system_prompt_hash = hashlib.sha256((system_prompt or "").encode()).hexdigest()[:16] - cache_key = (task_type, message[:100], system_prompt_hash) - if cache_key in self._routing_cache: + # Isolate cached responses by every stateless input that can affect output. + request_hash = hashlib.sha256(repr((message, temperature, max_tokens, retry_count)).encode()).hexdigest() + system_prompt_hash = hashlib.sha256((system_prompt or "").encode()).hexdigest() + cache_key = (task_type, request_hash, system_prompt_hash) + cacheable = conversation_id is None + if cacheable and cache_key in self._routing_cache: self.performance_metrics["cache_hits"] += 1 logger.debug(f"Cache hit for {task_type.value} task") return self._routing_cache[cache_key] # Evict oldest entry if cache is full - if len(self._routing_cache) >= self._max_cache_size: + if cacheable and len(self._routing_cache) >= self._max_cache_size: oldest_key = next(iter(self._routing_cache)) del self._routing_cache[oldest_key] logger.debug("Cache size limit reached, evicted oldest entry") @@ -358,7 +360,8 @@ def route_task( conversation_id, ) - self._routing_cache[cache_key] = (response, provider_key, cost) + if cacheable: + self._routing_cache[cache_key] = (response, provider_key, cost) self.performance_metrics["successful_routes"] += 1 self.performance_metrics["total_routing_time"] += time.time() - start_time diff --git a/pentestgpt/prompts/authorization.py b/pentestgpt/prompts/authorization.py index 6298cbd..6328d82 100644 --- a/pentestgpt/prompts/authorization.py +++ b/pentestgpt/prompts/authorization.py @@ -4,7 +4,7 @@ from collections.abc import Iterable -from pentestgpt.core.engagement import EngagementScope, normalize_target +from pentestgpt.core.engagement import EngagementScope, normalize_scope_target MITRE_PENTEST_ROLE = """You are an expert penetration tester with deep knowledge of the MITRE ATT&CK framework. Your responses should: @@ -40,7 +40,7 @@ def build_authorization_preamble( effective_targets: Iterable[str] = frozenset(), ) -> str: """Render neutral, authorized, or refusal framing for a target set.""" - targets = {normalized for target in effective_targets if (normalized := normalize_target(str(target)))} + targets = {str(target).strip() for target in effective_targets if str(target).strip()} if scope is None or not scope.is_configured(): return ( "ENGAGEMENT CONTEXT\n" @@ -62,7 +62,8 @@ def build_authorization_preamble( if scope.engagement_id: identity_parts.append(f"engagement_id={scope.engagement_id}") identity = f" ({', '.join(identity_parts)})" if identity_parts else "" - target_list = ", ".join(sorted(targets)) + rendered_targets = {normalize_scope_target(target) or target for target in targets} + target_list = ", ".join(sorted(rendered_targets)) roe = f" Rules of engagement: {scope.roe_notes}" if scope.roe_notes else "" return ( "AUTHORIZED ENGAGEMENT CONTEXT\n" diff --git a/tests/agents/test_results_verifier.py b/tests/agents/test_results_verifier.py index 7297462..2f126ee 100644 --- a/tests/agents/test_results_verifier.py +++ b/tests/agents/test_results_verifier.py @@ -1,11 +1,15 @@ """Tests for results_verifier.py.""" import json +from pathlib import Path from unittest.mock import MagicMock import pytest from pentestgpt.agents.results_verifier import ResultsVerifier +from pentestgpt.core.engagement import EngagementScope +from pentestgpt.core.exceptions import OutOfScopeError +from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard @pytest.fixture @@ -59,6 +63,31 @@ def test_verify_commands_error(self, verifier, mock_model_router): assert verified == commands + def test_verify_commands_extracts_targets_from_augmented_commands(self, tmp_path: Path) -> None: + router = MagicMock() + router.route_task.return_value = (json.dumps({"verified": True, "issues": []}), "mock", 0.0) + verifier = ResultsVerifier(model_router=router) + scope = EngagementScope(authorized_hosts=frozenset({"app.example.test"})) + verifier.set_engagement_scope(scope, ScopeGuard(scope, JsonlAuditSink(tmp_path / "audit.jsonl"))) + + commands = ["nmap -sV app.example.test -oX -"] + + assert verifier.verify_commands(commands) == commands + system_prompt = router.route_task.call_args.kwargs["system_prompt"] + assert "AUTHORIZED ENGAGEMENT CONTEXT" in system_prompt + assert "app.example.test" in system_prompt + + def test_verify_commands_propagates_out_of_scope_denial(self, tmp_path: Path) -> None: + router = MagicMock() + verifier = ResultsVerifier(model_router=router) + scope = EngagementScope(authorized_hosts=frozenset({"app.example.test"})) + verifier.set_engagement_scope(scope, ScopeGuard(scope, JsonlAuditSink(tmp_path / "audit.jsonl"))) + + with pytest.raises(OutOfScopeError): + verifier.verify_commands(["nmap outside.example.test -oX -"]) + + router.route_task.assert_not_called() + def test_analyze_results_success(self, verifier, mock_model_router): mock_response = { "status": "success", diff --git a/tests/core/test_engagement.py b/tests/core/test_engagement.py index 0e1525e..eb365bd 100644 --- a/tests/core/test_engagement.py +++ b/tests/core/test_engagement.py @@ -74,6 +74,17 @@ def test_configured_scope_rejects_invalid_target_alongside_authorized_target() - assert decision.offending == frozenset({"invalid/target"}) +def test_scope_accepts_only_cidr_targets_contained_by_authorized_ranges() -> None: + scope = EngagementScope(authorized_ip_ranges=("10.0.0.0/16",)) + + contained = scope.validate_targets({"10.0.4.12/24"}) + wider = scope.validate_targets({"10.0.0.0/8"}) + + assert contained.allowed is True + assert wider.allowed is False + assert wider.offending == frozenset({"10.0.0.0/8"}) + + def test_unconfigured_scope_is_neutral_and_non_authorizing() -> None: scope = EngagementScope() @@ -119,6 +130,28 @@ def test_extract_command_targets_finds_known_tool_positional_and_option_targets( assert extract_command_targets("curl --url https://192.0.2.25/status") == {"192.0.2.25"} +@pytest.mark.parametrize( + ("command", "expected"), + [ + ("nmap -sV app.example.test -oX -", {"app.example.test"}), + ("curl -u user:pass https://app.example.test", {"app.example.test"}), + ("ffuf -u https://app.example.test/FUZZ -w words.txt -t 40", {"app.example.test"}), + ("nuclei -u https://app.example.test -t cves/", {"app.example.test"}), + ("hydra -u -L users.txt -P passwords.txt ssh://app.example.test", {"app.example.test"}), + ("hydra app.example.test ssh -l user -p pass", {"app.example.test"}), + ("nxc smb app.example.test -u user -p pass", {"app.example.test"}), + ("nxc smb app.example.test -u user -p pass --domain CORP", {"app.example.test"}), + ("secretsdump.py domain/user:pass@app.example.test", {"app.example.test"}), + ("nmap 10.0.4.12/24 -oX -", {"10.0.4.0/24"}), + ], +) +def test_extract_command_targets_distinguishes_targets_from_tool_options( + command: str, + expected: set[str], +) -> None: + assert extract_command_targets(command) == expected + + def test_to_opsec_context_uses_normalized_canonical_scope() -> None: scope = EngagementScope( authorized_hosts=frozenset({"App.Example.Test"}), diff --git a/tests/core/test_scope_guard.py b/tests/core/test_scope_guard.py index 6c99cc4..015c046 100644 --- a/tests/core/test_scope_guard.py +++ b/tests/core/test_scope_guard.py @@ -35,6 +35,15 @@ def test_guard_audits_allow_without_opsec_session() -> None: assert sink.entries[0]["targets"] == ["10.0.0.8"] +def test_guard_retains_allowed_cidr_for_downstream_prompt_context() -> None: + sink = MemoryAuditSink() + guard = ScopeGuard(EngagementScope(authorized_ip_ranges=("10.0.0.0/16",)), sink) + + guard.check("command_generation", {"10.0.4.12/24"}) + + assert guard.last_allowed_targets == frozenset({"10.0.4.0/24"}) + + def test_guard_audits_deny_then_raises() -> None: sink = MemoryAuditSink() guard = ScopeGuard( diff --git a/tests/integration/test_scope_guard_lifecycle.py b/tests/integration/test_scope_guard_lifecycle.py index 8829b01..7672585 100644 --- a/tests/integration/test_scope_guard_lifecycle.py +++ b/tests/integration/test_scope_guard_lifecycle.py @@ -58,6 +58,11 @@ async def test_scope_guard_blocks_discovered_out_of_scope_host_before_llm_and_ex executor.execute_batch_async.assert_not_called() executor.execute_batch.assert_not_called() + with pytest.raises(OutOfScopeError): + await orchestrator._dispatch_executor(["nmap outside.example.test -oX -"]) + executor.execute_batch_async.assert_not_called() + executor.execute_batch.assert_not_called() + orchestrator._session_findings["discovered_hosts"].append("192.0.2.25") router.reset_mock() diff --git a/tests/llms/test_router_cache_isolation.py b/tests/llms/test_router_cache_isolation.py index 25eba56..d5e1e6a 100644 --- a/tests/llms/test_router_cache_isolation.py +++ b/tests/llms/test_router_cache_isolation.py @@ -39,3 +39,59 @@ def test_identical_system_prompt_still_hits_cache() -> None: router.route_task(TaskType.ANALYSIS, "same message", system_prompt="same scope") provider.send_new_conversation.assert_called_once() + + +def test_messages_with_same_prefix_use_distinct_cache_entries() -> None: + provider = MagicMock(spec=LLMProvider) + provider.send_new_conversation.side_effect = [("first", "c1"), ("second", "c2")] + provider.count_tokens.return_value = 1 + provider.get_cost_estimate.return_value = 0.0 + router = ModelRouter( + {"provider:model": provider}, + routing_rules={TaskType.ANALYSIS: ["provider:model"]}, + ) + common_prefix = "x" * 100 + + first = router.route_task(TaskType.ANALYSIS, f"{common_prefix}-first", system_prompt="same scope") + second = router.route_task(TaskType.ANALYSIS, f"{common_prefix}-second", system_prompt="same scope") + + assert first[0] == "first" + assert second[0] == "second" + assert provider.send_new_conversation.call_count == 2 + + +def test_conversation_continuations_bypass_stateless_cache() -> None: + provider = MagicMock(spec=LLMProvider) + provider.send_message.side_effect = ["first", "second"] + provider.count_tokens.return_value = 1 + provider.get_cost_estimate.return_value = 0.0 + router = ModelRouter( + {"provider:model": provider}, + routing_rules={TaskType.ANALYSIS: ["provider:model"]}, + ) + + first = router.route_task(TaskType.ANALYSIS, "same message", conversation_id="conversation") + second = router.route_task(TaskType.ANALYSIS, "same message", conversation_id="conversation") + + assert first[0] == "first" + assert second[0] == "second" + assert provider.send_message.call_count == 2 + assert router._routing_cache == {} + + +def test_generation_parameters_are_part_of_cache_identity() -> None: + provider = MagicMock(spec=LLMProvider) + provider.send_new_conversation.side_effect = [("first", "c1"), ("second", "c2")] + provider.count_tokens.return_value = 1 + provider.get_cost_estimate.return_value = 0.0 + router = ModelRouter( + {"provider:model": provider}, + routing_rules={TaskType.ANALYSIS: ["provider:model"]}, + ) + + first = router.route_task(TaskType.ANALYSIS, "same message", temperature=0.1) + second = router.route_task(TaskType.ANALYSIS, "same message", temperature=0.9) + + assert first[0] == "first" + assert second[0] == "second" + assert provider.send_new_conversation.call_count == 2 diff --git a/tests/prompts/test_authorization_preamble.py b/tests/prompts/test_authorization_preamble.py index 5213446..bc55d32 100644 --- a/tests/prompts/test_authorization_preamble.py +++ b/tests/prompts/test_authorization_preamble.py @@ -47,3 +47,21 @@ def test_configured_scope_with_missing_targets_renders_refusal() -> None: preamble = build_authorization_preamble(scope) assert "requires at least one effective target" in preamble + + +def test_preamble_rejects_invalid_target_alongside_authorized_target() -> None: + scope = EngagementScope(authorized_hosts=frozenset({"lab.example.test"})) + + preamble = build_authorization_preamble(scope, {"lab.example.test", "invalid/target"}) + + assert "ENGAGEMENT SCOPE VIOLATION" in preamble + assert "invalid/target" in preamble + + +def test_preamble_accepts_cidr_contained_by_authorized_range() -> None: + scope = EngagementScope(authorized_ip_ranges=("10.0.0.0/16",)) + + preamble = build_authorization_preamble(scope, {"10.0.4.12/24"}) + + assert "AUTHORIZED ENGAGEMENT CONTEXT" in preamble + assert "10.0.4.0/24" in preamble From 96a1640da36d30e42ae2ef037a45cfd05e21a299 Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:03:35 +0800 Subject: [PATCH 09/12] Fix command scope extraction safeguards --- .gitignore | 2 +- pentestgpt/agents/results_verifier.py | 13 ++- pentestgpt/core/engagement.py | 106 +++++++++++++++--- pentestgpt/orchestrator.py | 6 +- tests/agents/test_results_verifier.py | 11 ++ tests/core/test_engagement.py | 24 +++- .../integration/test_scope_guard_lifecycle.py | 12 ++ 7 files changed, 154 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 1c650bd..609f69a 100644 --- a/.gitignore +++ b/.gitignore @@ -211,7 +211,7 @@ pytest_full_output.txt final_mypy_results.txt final_final_mypy_results.txt coverage.json -bandit_results.json +bandit-report.json # Generated reports reports/ diff --git a/pentestgpt/agents/results_verifier.py b/pentestgpt/agents/results_verifier.py index faffc8b..1e5dd3b 100644 --- a/pentestgpt/agents/results_verifier.py +++ b/pentestgpt/agents/results_verifier.py @@ -14,7 +14,10 @@ from loguru import logger from pentestgpt.agents.parsers.registry import ParserRegistry -from pentestgpt.core.engagement import extract_command_targets +from pentestgpt.core.engagement import ( + INCOMPLETE_COMMAND_TARGET, + extract_command_targets, +) from pentestgpt.core.exceptions import OutOfScopeError from pentestgpt.llms.router import ModelRouter, TaskType from pentestgpt.mitre import MITRETechnique @@ -66,7 +69,13 @@ def get_health_status(self) -> dict[str, Any]: @staticmethod def _command_targets(commands: list[str]) -> set[str]: """Extract effective targets without treating commands as host strings.""" - return {target for command in commands for target in extract_command_targets(command)} + targets: set[str] = set() + for command in commands: + command_targets, extraction_complete = extract_command_targets(command) + targets.update(command_targets) + if not extraction_complete: + targets.add(INCOMPLETE_COMMAND_TARGET) + return targets def verify_commands( self, diff --git a/pentestgpt/core/engagement.py b/pentestgpt/core/engagement.py index 36e19c9..b09a756 100644 --- a/pentestgpt/core/engagement.py +++ b/pentestgpt/core/engagement.py @@ -136,9 +136,46 @@ _POSITIONAL_SECOND_TARGET_TOOLS = frozenset({"cme", "crackmapexec", "netexec", "nxc"}) _POSITIONAL_LAST_TARGET_TOOLS = frozenset({"dnsenum", "enum4linux", "enum4linux-ng", "hping3"}) _COMMAND_WRAPPERS_WITH_ARGS: dict[str, int] = {"stdbuf": 1, "timeout": 1} -_COMMAND_WRAPPERS_WITHOUT_ARGS = frozenset( - {"env", "ionice", "ltrace", "nice", "nohup", "setsid", "strace", "sudo", "unbuffer"} +_COMMAND_WRAPPERS_WITHOUT_ARGS = frozenset({"env", "ionice", "ltrace", "nice", "nohup", "setsid", "strace", "unbuffer"}) +_SUDO_OPTIONS_WITH_ARGS = frozenset( + {"-C", "-g", "-h", "-r", "-t", "-u", "--close-from", "--group", "--host", "--role", "--type", "--user"} ) +_SUDO_OPTIONS_WITHOUT_ARGS = frozenset( + { + "-A", + "-b", + "-E", + "-H", + "-K", + "-k", + "-n", + "-P", + "-S", + "-V", + "-v", + "--askpass", + "--background", + "--non-interactive", + "--preserve-env", + "--reset-timestamp", + "--remove-timestamp", + "--set-home", + "--stdin", + "--validate", + "--version", + } +) +_SUPPORTED_COMMAND_TOOLS = ( + frozenset(_TOOL_COMMAND_TARGET_OPTIONS) + | frozenset(_TOOL_COMMAND_VALUE_OPTIONS) + | _POSITIONAL_ALL_TARGET_TOOLS + | _POSITIONAL_FIRST_TARGET_TOOLS + | _POSITIONAL_SECOND_TARGET_TOOLS + | _POSITIONAL_LAST_TARGET_TOOLS + | frozenset({"secretsdump.py"}) +) +INCOMPLETE_COMMAND_TARGET = "" +_UNPARSEABLE_COMMAND_TARGET = "" def normalize_target(value: str) -> str: @@ -224,20 +261,22 @@ def visit_mapping(mapping: Mapping[Any, Any]) -> None: return targets -def extract_command_targets(command: str) -> set[str]: +def extract_command_targets(command: str) -> tuple[set[str], bool]: """Extract explicit targets from a command before it reaches an executor. URL, IP, and CIDR literals are recognized wherever they occur. Target and non-target options are interpreted per tool so credentials, thread counts, output paths, and template names are not mistaken for hosts. Malformed - target values are retained so configured scopes deny them. + target values are retained so configured scopes deny them. The second + return value is false when target extraction is incomplete; configured + scopes must deny such commands rather than trust prior authorized targets. """ try: tokens = shlex.split(command) except ValueError: - return {command.strip()} if command.strip() else set() + return ({_UNPARSEABLE_COMMAND_TARGET}, False) if command.strip() else (set(), True) if not tokens: - return set() + return set(), True targets: set[str] = set() @@ -246,7 +285,8 @@ def add_target(value: str) -> None: if candidate: targets.add(normalize_scope_target(candidate) or candidate) - tool, tool_index = _extract_command_tool(tokens) + tool, tool_index, wrappers_complete = _extract_command_tool(tokens) + extraction_complete = wrappers_complete and tool in _SUPPORTED_COMMAND_TOOLS target_options = _COMMON_COMMAND_TARGET_OPTIONS | _TOOL_COMMAND_TARGET_OPTIONS.get(tool, frozenset()) value_options = _TOOL_COMMAND_VALUE_OPTIONS.get(tool, frozenset()) positional: list[str] = [] @@ -285,25 +325,65 @@ def add_target(value: str) -> None: add_target(positional[1]) elif tool in _POSITIONAL_LAST_TARGET_TOOLS and positional: add_target(positional[-1]) - return targets + elif tool == "secretsdump.py" and positional and "@" not in positional[0]: + add_target(positional[0]) + + if not extraction_complete: + for candidate in positional: + normalized = normalize_scope_target(candidate) + if _is_ip_target(normalized) or "." in normalized: + add_target(candidate) + return targets, extraction_complete -def _extract_command_tool(tokens: list[str]) -> tuple[str, int]: - """Return the unwrapped command name and its token index.""" +def _extract_command_tool(tokens: list[str]) -> tuple[str, int, bool]: + """Return the unwrapped command name, token index, and parsing status.""" index = 0 + complete = True while index < len(tokens): token = tokens[index] if _ENV_ASSIGNMENT_RE.match(token): index += 1 continue + if token == "sudo": + index, sudo_complete = _skip_sudo_options(tokens, index + 1) + complete = complete and sudo_complete + continue if token in _COMMAND_WRAPPERS_WITHOUT_ARGS: index += 1 continue if token in _COMMAND_WRAPPERS_WITH_ARGS: - index += 1 + _COMMAND_WRAPPERS_WITH_ARGS[token] + argument_count = _COMMAND_WRAPPERS_WITH_ARGS[token] + if index + argument_count >= len(tokens): + return "", len(tokens), False + index += 1 + argument_count + continue + return token.rsplit("/", 1)[-1].casefold(), index, complete + return "", len(tokens), False + + +def _skip_sudo_options(tokens: list[str], index: int) -> tuple[int, bool]: + """Skip supported sudo options and report whether their parsing was certain.""" + while index < len(tokens): + token = tokens[index] + option, separator, _value = token.partition("=") + if token == "--": + return index + 1, True + if option in _SUDO_OPTIONS_WITH_ARGS: + if separator: + index += 1 + continue + if index + 1 >= len(tokens): + return len(tokens), False + index += 2 + continue + if option in _SUDO_OPTIONS_WITHOUT_ARGS: + index += 1 continue - return token.rsplit("/", 1)[-1].casefold(), index - return "", len(tokens) + if token.startswith("-"): + return index + 1, False + return index, True + return len(tokens), False def _is_ip_target(value: str) -> bool: diff --git a/pentestgpt/orchestrator.py b/pentestgpt/orchestrator.py index 80b0c1a..3087195 100644 --- a/pentestgpt/orchestrator.py +++ b/pentestgpt/orchestrator.py @@ -45,6 +45,7 @@ ) from pentestgpt.config.stealth_config import StealthConfig from pentestgpt.core.engagement import ( + INCOMPLETE_COMMAND_TARGET, EngagementScope, extract_command_targets, extract_effective_targets, @@ -609,7 +610,10 @@ async def _dispatch_executor(self, verified_commands: list[str]) -> dict[str, An """Dispatch commands to the appropriate executor interface.""" command_targets: set[str] = set() for command in verified_commands: - command_targets.update(extract_command_targets(command)) + targets, extraction_complete = extract_command_targets(command) + command_targets.update(targets) + if not extraction_complete: + command_targets.add(INCOMPLETE_COMMAND_TARGET) self.scope_guard.check("command_execution", self._effective_targets() | command_targets) if hasattr(self.executor, "execute_batch_async") and asyncio.iscoroutinefunction( getattr(self.executor, "execute_batch_async", None) diff --git a/tests/agents/test_results_verifier.py b/tests/agents/test_results_verifier.py index 2f126ee..bc67f1a 100644 --- a/tests/agents/test_results_verifier.py +++ b/tests/agents/test_results_verifier.py @@ -88,6 +88,17 @@ def test_verify_commands_propagates_out_of_scope_denial(self, tmp_path: Path) -> router.route_task.assert_not_called() + def test_verify_commands_rejects_incomplete_target_extraction(self, tmp_path: Path) -> None: + router = MagicMock() + verifier = ResultsVerifier(model_router=router) + scope = EngagementScope(authorized_hosts=frozenset({"app.example.test"})) + verifier.set_engagement_scope(scope, ScopeGuard(scope, JsonlAuditSink(tmp_path / "audit.jsonl"))) + + with pytest.raises(OutOfScopeError): + verifier.verify_commands(["ping app.example.test"]) + + router.route_task.assert_not_called() + def test_analyze_results_success(self, verifier, mock_model_router): mock_response = { "status": "success", diff --git a/tests/core/test_engagement.py b/tests/core/test_engagement.py index eb365bd..8a976ef 100644 --- a/tests/core/test_engagement.py +++ b/tests/core/test_engagement.py @@ -126,8 +126,8 @@ def test_extract_effective_targets_includes_primary_and_discovered_hosts() -> No def test_extract_command_targets_finds_known_tool_positional_and_option_targets() -> None: - assert extract_command_targets("nmap -sV app.example.test") == {"app.example.test"} - assert extract_command_targets("curl --url https://192.0.2.25/status") == {"192.0.2.25"} + assert extract_command_targets("nmap -sV app.example.test") == ({"app.example.test"}, True) + assert extract_command_targets("curl --url https://192.0.2.25/status") == ({"192.0.2.25"}, True) @pytest.mark.parametrize( @@ -149,7 +149,25 @@ def test_extract_command_targets_distinguishes_targets_from_tool_options( command: str, expected: set[str], ) -> None: - assert extract_command_targets(command) == expected + assert extract_command_targets(command) == (expected, True) + + +def test_extract_command_targets_marks_malformed_commands_incomplete_without_auditing_them() -> None: + targets, complete = extract_command_targets("curl -u user:password@host '") + + assert targets == {""} + assert complete is False + + +def test_extract_command_targets_marks_unsupported_commands_incomplete_and_keeps_bare_hostnames() -> None: + targets, complete = extract_command_targets("ping outside.example.test") + + assert targets == {"outside.example.test"} + assert complete is False + + +def test_extract_command_targets_unwraps_sudo_options() -> None: + assert extract_command_targets("sudo -u root nmap app.example.test") == ({"app.example.test"}, True) def test_to_opsec_context_uses_normalized_canonical_scope() -> None: diff --git a/tests/integration/test_scope_guard_lifecycle.py b/tests/integration/test_scope_guard_lifecycle.py index 7672585..3d109be 100644 --- a/tests/integration/test_scope_guard_lifecycle.py +++ b/tests/integration/test_scope_guard_lifecycle.py @@ -63,6 +63,17 @@ async def test_scope_guard_blocks_discovered_out_of_scope_host_before_llm_and_ex executor.execute_batch_async.assert_not_called() executor.execute_batch.assert_not_called() + with pytest.raises(OutOfScopeError): + await orchestrator._dispatch_executor(["ping 10.0.0.5"]) + executor.execute_batch_async.assert_not_called() + executor.execute_batch.assert_not_called() + + malformed_command = "curl -u user:password@host '" + with pytest.raises(OutOfScopeError): + await orchestrator._dispatch_executor([malformed_command]) + executor.execute_batch_async.assert_not_called() + executor.execute_batch.assert_not_called() + orchestrator._session_findings["discovered_hosts"].append("192.0.2.25") router.reset_mock() @@ -78,6 +89,7 @@ async def test_scope_guard_blocks_discovered_out_of_scope_host_before_llm_and_ex entries = [json.loads(line) for line in audit_path.read_text(encoding="utf-8").splitlines()] assert any(entry["decision"] == "allow" for entry in entries) assert any(entry["decision"] == "deny" and entry["offending_targets"] == ["192.0.2.25"] for entry in entries) + assert all(malformed_command not in json.dumps(entry) for entry in entries) def test_direct_orchestrator_rejects_mismatched_scope_guard(tmp_path: Path) -> None: From 399df92d2d7f434a6d9b1ccce862984e693f3d69 Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:33:11 +0800 Subject: [PATCH 10/12] fix: validate every host in protocol-prefixed and multi-URL commands extract_command_targets only validated the second positional of nxc, netexec, crackmapexec, and cme commands, so extra hostname targets in a multi-target command bypassed scope enforcement (e.g. an out-of-scope host alongside an authorized one executed unchecked). Treat these tools as protocol-then-targets and validate positional[1:], and move curl to the all-positional set since every curl positional is a URL target. --- pentestgpt/core/engagement.py | 14 ++++++++------ tests/core/test_engagement.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/pentestgpt/core/engagement.py b/pentestgpt/core/engagement.py index b09a756..2ea38a2 100644 --- a/pentestgpt/core/engagement.py +++ b/pentestgpt/core/engagement.py @@ -131,9 +131,10 @@ ), "nuclei": frozenset({"-H", "-dast-server", "-o", "-rl", "-t", "-w", "--templates", "--workflows"}), } -_POSITIONAL_ALL_TARGET_TOOLS = frozenset({"masscan", "nmap", "nuclei"}) -_POSITIONAL_FIRST_TARGET_TOOLS = frozenset({"curl", "dirb", "hydra"}) -_POSITIONAL_SECOND_TARGET_TOOLS = frozenset({"cme", "crackmapexec", "netexec", "nxc"}) +_POSITIONAL_ALL_TARGET_TOOLS = frozenset({"curl", "masscan", "nmap", "nuclei"}) +_POSITIONAL_FIRST_TARGET_TOOLS = frozenset({"dirb", "hydra"}) +# First positional is a protocol selector; every later positional is a target host. +_PROTOCOL_PREFIXED_TARGET_TOOLS = frozenset({"cme", "crackmapexec", "netexec", "nxc"}) _POSITIONAL_LAST_TARGET_TOOLS = frozenset({"dnsenum", "enum4linux", "enum4linux-ng", "hping3"}) _COMMAND_WRAPPERS_WITH_ARGS: dict[str, int] = {"stdbuf": 1, "timeout": 1} _COMMAND_WRAPPERS_WITHOUT_ARGS = frozenset({"env", "ionice", "ltrace", "nice", "nohup", "setsid", "strace", "unbuffer"}) @@ -170,7 +171,7 @@ | frozenset(_TOOL_COMMAND_VALUE_OPTIONS) | _POSITIONAL_ALL_TARGET_TOOLS | _POSITIONAL_FIRST_TARGET_TOOLS - | _POSITIONAL_SECOND_TARGET_TOOLS + | _PROTOCOL_PREFIXED_TARGET_TOOLS | _POSITIONAL_LAST_TARGET_TOOLS | frozenset({"secretsdump.py"}) ) @@ -321,8 +322,9 @@ def add_target(value: str) -> None: add_target(candidate) elif tool in _POSITIONAL_FIRST_TARGET_TOOLS and positional: add_target(positional[0]) - elif tool in _POSITIONAL_SECOND_TARGET_TOOLS and len(positional) > 1: - add_target(positional[1]) + elif tool in _PROTOCOL_PREFIXED_TARGET_TOOLS and len(positional) > 1: + for candidate in positional[1:]: + add_target(candidate) elif tool in _POSITIONAL_LAST_TARGET_TOOLS and positional: add_target(positional[-1]) elif tool == "secretsdump.py" and positional and "@" not in positional[0]: diff --git a/tests/core/test_engagement.py b/tests/core/test_engagement.py index 8a976ef..a621b06 100644 --- a/tests/core/test_engagement.py +++ b/tests/core/test_engagement.py @@ -141,6 +141,12 @@ def test_extract_command_targets_finds_known_tool_positional_and_option_targets( ("hydra app.example.test ssh -l user -p pass", {"app.example.test"}), ("nxc smb app.example.test -u user -p pass", {"app.example.test"}), ("nxc smb app.example.test -u user -p pass --domain CORP", {"app.example.test"}), + ( + "nxc smb 10.0.0.5 evil.example.test -u user -p pass", + {"10.0.0.5", "evil.example.test"}, + ), + ("netexec smb dc1.corp.test dc2.corp.test", {"dc1.corp.test", "dc2.corp.test"}), + ("curl a.example.test b.example.test", {"a.example.test", "b.example.test"}), ("secretsdump.py domain/user:pass@app.example.test", {"app.example.test"}), ("nmap 10.0.4.12/24 -oX -", {"10.0.4.0/24"}), ], @@ -152,6 +158,16 @@ def test_extract_command_targets_distinguishes_targets_from_tool_options( assert extract_command_targets(command) == (expected, True) +def test_multi_target_protocol_tool_denies_out_of_scope_secondary_host() -> None: + scope = EngagementScope(authorized_ip_ranges=("10.0.0.0/24",)) + targets, complete = extract_command_targets("nxc smb 10.0.0.5 evil.example.test -u user -p pass") + + assert complete is True + decision = scope.validate_targets(targets) + assert decision.allowed is False + assert decision.offending == frozenset({"evil.example.test"}) + + def test_extract_command_targets_marks_malformed_commands_incomplete_without_auditing_them() -> None: targets, complete = extract_command_targets("curl -u user:password@host '") From c11536cbccb13d226d01be27c7ac9bfebfd3aa30 Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:33:29 +0800 Subject: [PATCH 11/12] fix: preserve externally shared scope guard on OPSEC session creation OPSECAgent.create_session rebuilt its ScopeGuard unconditionally, so a guard injected by the orchestrator via set_engagement_scope was replaced by a session-scoped one, splitting the unified audit trail and resetting shared last-allowed-target state. Track whether the guard was injected externally and only build a session-scoped guard when the agent owns it. --- pentestgpt/agents/opsec_agent.py | 19 ++++++++++++++----- tests/agents/test_opsec_agent.py | 16 +++++++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pentestgpt/agents/opsec_agent.py b/pentestgpt/agents/opsec_agent.py index bd07b2c..217e120 100644 --- a/pentestgpt/agents/opsec_agent.py +++ b/pentestgpt/agents/opsec_agent.py @@ -500,9 +500,15 @@ def __init__( self.current_session: OPSECSession | None = None self.session_history: list[OPSECSession] = [] self.database_manager: Any = None + self._external_scope_guard = False logger.info("OPSEC Agent initialized") + def set_engagement_scope(self, scope: EngagementScope | None, guard: ScopeGuard) -> None: + """Adopt an externally shared scope guard instead of a session-scoped one.""" + super().set_engagement_scope(scope, guard) + self._external_scope_guard = True + def _get_metadata(self) -> AgentMetadata: """Get agent metadata.""" return AgentMetadata( @@ -528,11 +534,14 @@ def create_session(self, scope: EngagementScope | None = None) -> str: """ session_id = str(uuid4())[:8] # Short ID for display - # Create session with current configuration - if scope is not None: - self._engagement_scope = scope - audit_sink = JsonlAuditSink(Path(self.session_dir) / session_id / "scope_audit.jsonl") - self._scope_guard = ScopeGuard(self._engagement_scope, audit_sink) + # A guard injected via set_engagement_scope is shared across the whole + # engagement (unified audit log and last-allowed-target state); only build + # a session-scoped guard when this agent owns its guard. + if not self._external_scope_guard: + if scope is not None: + self._engagement_scope = scope + audit_sink = JsonlAuditSink(Path(self.session_dir) / session_id / "scope_audit.jsonl") + self._scope_guard = ScopeGuard(self._engagement_scope, audit_sink) session = OPSECSession(session_id, self.config.opsec, self._engagement_scope) session.start_session() diff --git a/tests/agents/test_opsec_agent.py b/tests/agents/test_opsec_agent.py index 6473f14..66262f8 100644 --- a/tests/agents/test_opsec_agent.py +++ b/tests/agents/test_opsec_agent.py @@ -19,8 +19,9 @@ StealthConfig, StealthLevel, ) +from pentestgpt.core.engagement import EngagementScope from pentestgpt.core.exceptions import OutOfScopeError -from pentestgpt.core.scope_guard import JsonlAuditSink +from pentestgpt.core.scope_guard import JsonlAuditSink, ScopeGuard from tests import make_mock_router # --------------------------------------------------------------------------- @@ -320,6 +321,19 @@ def test_create_session_audit_uses_configured_session_dir(self, tmp_path: Any) - assert second_sink.path == Path(tmp_path) / second_session_id / "scope_audit.jsonl" assert first_sink.path == Path(tmp_path) / session_id / "scope_audit.jsonl" + def test_create_session_preserves_externally_injected_scope_guard(self, tmp_path: Any) -> None: + agent = _make_opsec_agent() + scope = EngagementScope(authorized_hosts=frozenset({"lab.example.test"})) + shared_guard = ScopeGuard(scope, JsonlAuditSink(tmp_path / "shared_audit.jsonl")) + agent.set_engagement_scope(scope, shared_guard) + + agent.create_session() + + # A guard shared by the orchestrator must survive session creation so the + # audit trail and last-allowed-target state stay unified. + assert agent._scope_guard is shared_guard + assert agent._engagement_scope is scope + def test_end_current_session_clears_current(self) -> None: agent = _make_opsec_agent() agent.create_session() From ff1e78b24cf7bb25261137aac7b61057f0e5776b Mon Sep 17 00:00:00 2001 From: SALhik <123875913+SALhik@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:33:46 +0800 Subject: [PATCH 12/12] test: de-brittle route_task preamble coverage guard Replace the hardcoded `assert calls == 40` with a non-zero floor and swap the line-numbered benchmark allowlist for a whole-file entry, so adding or moving an unrelated route_task call no longer breaks the test. The invariant that production route_task calls carry a preamble-bearing system_prompt stays enforced by the violations check. --- tests/prompts/test_route_task_coverage.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/prompts/test_route_task_coverage.py b/tests/prompts/test_route_task_coverage.py index 9c8425c..7538dce 100644 --- a/tests/prompts/test_route_task_coverage.py +++ b/tests/prompts/test_route_task_coverage.py @@ -5,7 +5,8 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[2] PACKAGE_ROOT = REPOSITORY_ROOT / "pentestgpt" -BENCHMARK_ALLOWLIST = {("utils/performance_benchmark.py", 260)} +# Benchmark modules exercise the router directly and intentionally omit framing. +BENCHMARK_ALLOWLIST = {"utils/performance_benchmark.py"} PREAMBLE_BUILDERS = { "get_system_prompt", "build_system_prompt", @@ -27,7 +28,7 @@ def test_every_route_task_call_has_a_preamble_bearing_system_prompt() -> None: ): continue calls += 1 - if (relative, node.lineno) in BENCHMARK_ALLOWLIST: + if relative in BENCHMARK_ALLOWLIST: continue system_prompt = next( (keyword.value for keyword in node.keywords if keyword.arg == "system_prompt"), @@ -45,5 +46,5 @@ def test_every_route_task_call_has_a_preamble_bearing_system_prompt() -> None: if name not in PREAMBLE_BUILDERS: violations.append(f"{relative}:{node.lineno} uses {name or 'an unknown expression'}") - assert calls == 40 + assert calls > 0, "no route_task calls were discovered; the AST walk is not scanning the package" assert not violations, "\n".join(violations)