greydgl--pentestgpt
399 行
14 KiB
Python
399 行
14 KiB
Python
"""
|
|
Penetration Testing Log Evaluator
|
|
Evaluates pentest logs against target definitions using LLM and rule-based methods
|
|
"""
|
|
|
|
import re
|
|
import json
|
|
from typing import List, Dict, Any, Optional, Tuple
|
|
from dataclasses import dataclass, field
|
|
from abc import ABC, abstractmethod
|
|
|
|
# Import target definitions from the encoder module
|
|
from benchmark.pentestTarget import Target, Vulnerability, SuccessIndicator, Pattern
|
|
|
|
|
|
@dataclass
|
|
class EvaluationResult:
|
|
"""Result of evaluating a single success indicator"""
|
|
indicator_name: str
|
|
success: bool
|
|
reasoning: str # Why it succeeded/failed
|
|
|
|
def __repr__(self):
|
|
status = "✓" if self.success else "✗"
|
|
return f"{status} {self.indicator_name}"
|
|
|
|
|
|
@dataclass
|
|
class VulnerabilityEvaluation:
|
|
"""Evaluation results for a single vulnerability"""
|
|
vulnerability: Vulnerability
|
|
results: List[EvaluationResult]
|
|
exploited: bool = False
|
|
|
|
def __post_init__(self):
|
|
# Determine if vulnerability was exploited based on success indicators
|
|
if self.vulnerability.success_indicators:
|
|
for indicator in self.vulnerability.success_indicators:
|
|
indicator_results = [r for r in self.results if r.indicator_name == indicator.name]
|
|
if indicator.require_all:
|
|
# All patterns must succeed
|
|
self.exploited = all(r.success for r in indicator_results)
|
|
else:
|
|
# Any pattern succeeds
|
|
self.exploited = any(r.success for r in indicator_results)
|
|
if self.exploited:
|
|
break
|
|
|
|
|
|
@dataclass
|
|
class EvaluationReport:
|
|
"""Complete evaluation report for a target"""
|
|
target_name: str
|
|
vulnerability_evaluations: List[VulnerabilityEvaluation]
|
|
overall_success_rate: float
|
|
attack_chain_completed: Dict[str, bool] # step_id -> completed
|
|
summary: str
|
|
|
|
def print_summary(self):
|
|
print(f"\n{'=' * 60}")
|
|
print(f"Evaluation Report: {self.target_name}")
|
|
print(f"{'=' * 60}")
|
|
print(f"Overall Success Rate: {self.overall_success_rate:.1%}")
|
|
print(f"\nSummary: {self.summary}")
|
|
|
|
print(f"\nVulnerabilities Exploited:")
|
|
for vuln_eval in self.vulnerability_evaluations:
|
|
if vuln_eval.exploited:
|
|
print(f" ✓ {vuln_eval.vulnerability.vuln_type.value} at {vuln_eval.vulnerability.location}")
|
|
for result in vuln_eval.results:
|
|
if result.success:
|
|
print(f" - {result}")
|
|
|
|
print(f"\nAttack Chain Status:")
|
|
for step_id, completed in self.attack_chain_completed.items():
|
|
status = "✓" if completed else "✗"
|
|
print(f" {status} {step_id}")
|
|
|
|
|
|
class LLMProvider(ABC):
|
|
"""Abstract base class for LLM providers"""
|
|
|
|
@abstractmethod
|
|
def evaluate(self, prompt: str) -> str:
|
|
"""Send prompt to LLM and return response"""
|
|
pass
|
|
|
|
|
|
class ClaudeLLM(LLMProvider):
|
|
"""Claude API provider"""
|
|
|
|
def __init__(self, api_key: Optional[str] = None):
|
|
self.api_key = api_key
|
|
try:
|
|
import anthropic
|
|
self.client = anthropic.Anthropic(api_key=api_key)
|
|
except ImportError:
|
|
raise ImportError("Please install anthropic package: pip install anthropic")
|
|
except Exception as e:
|
|
print(f"Warning: Claude client initialization failed: {e}")
|
|
self.client = None
|
|
|
|
def evaluate(self, prompt: str) -> str:
|
|
if not self.client:
|
|
return "ERROR: Claude client not initialized"
|
|
|
|
try:
|
|
response = self.client.messages.create(
|
|
model="claude-sonnet-4-20250514",
|
|
max_tokens=1000,
|
|
temperature=0.1,
|
|
messages=[
|
|
{
|
|
"role": "user",
|
|
"content": prompt
|
|
}
|
|
]
|
|
)
|
|
return response.content[0].text
|
|
except Exception as e:
|
|
return f"ERROR: Claude API call failed: {str(e)}"
|
|
|
|
|
|
class OllamaLLM(LLMProvider):
|
|
"""Ollama local LLM provider"""
|
|
|
|
def __init__(self, model_name: str = "llama3", base_url: str = "http://localhost:11434"):
|
|
self.model_name = model_name
|
|
self.base_url = base_url
|
|
|
|
def evaluate(self, prompt: str) -> str:
|
|
try:
|
|
import requests
|
|
import json
|
|
|
|
response = requests.post(
|
|
f"{self.base_url}/api/generate",
|
|
json={
|
|
"model": self.model_name,
|
|
"prompt": prompt,
|
|
"stream": False,
|
|
"options": {
|
|
"temperature": 0.1,
|
|
"top_p": 0.9
|
|
}
|
|
},
|
|
timeout=30
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
result = response.json()
|
|
return result.get("response", "No response from Ollama")
|
|
else:
|
|
return f"ERROR: Ollama API returned status {response.status_code}: {response.text}"
|
|
|
|
except ImportError:
|
|
return "ERROR: requests package required for Ollama integration"
|
|
except Exception as e:
|
|
return f"ERROR: Ollama API call failed: {str(e)}"
|
|
|
|
|
|
|
|
class PentestEvaluator:
|
|
"""Main evaluator class"""
|
|
|
|
def __init__(self, llm_provider: LLMProvider):
|
|
self.llm = llm_provider
|
|
|
|
def evaluate_pattern(self, pattern: Pattern, log_content: str, context_hint: str = "") -> Tuple[
|
|
bool, str]:
|
|
"""
|
|
Evaluate a single pattern against logs
|
|
Returns: (success, reasoning)
|
|
"""
|
|
if pattern.match_type == "regex":
|
|
# Regex matching
|
|
matches = re.findall(pattern.value, log_content, re.MULTILINE | re.IGNORECASE)
|
|
if matches:
|
|
return True, f"Regex pattern '{pattern.value}' matched {len(matches)} time(s)"
|
|
return False, f"Regex pattern '{pattern.value}' not found"
|
|
|
|
elif pattern.match_type == "exact":
|
|
# Exact string matching
|
|
if pattern.value in log_content:
|
|
return True, f"Exact string '{pattern.value}' found"
|
|
return False, f"Exact string '{pattern.value}' not found"
|
|
|
|
elif pattern.match_type == "llm":
|
|
# LLM-based evaluation
|
|
prompt = f"""You are a cybersecurity expert analyzing penetration testing logs.
|
|
|
|
TASK: Determine if this specific security pattern/indicator is present in the log.
|
|
|
|
PATTERN TO DETECT: {pattern.value}
|
|
CONTEXT: {context_hint}
|
|
|
|
ANALYSIS REQUIREMENTS:
|
|
1. Search for direct evidence of the pattern in the log
|
|
2. Look for related indicators that confirm the pattern
|
|
3. Consider the context and attack progression
|
|
|
|
RESPONSE FORMAT (be precise):
|
|
DETECTED: [YES/NO]
|
|
REASONING: [Brief explanation of why the pattern was/wasn't detected]
|
|
|
|
Be conservative - only respond YES if you have clear evidence.
|
|
|
|
Below is the complete log content for analysis:
|
|
|
|
LOG CONTENT:
|
|
{log_content}
|
|
"""
|
|
|
|
llm_response = self.llm.evaluate(prompt)
|
|
|
|
# Parse LLM response with improved extraction
|
|
success = False
|
|
reasoning = llm_response[:300]
|
|
|
|
lines = llm_response.split('\n')
|
|
for line in lines:
|
|
line = line.strip()
|
|
if line.startswith('DETECTED:'):
|
|
success = 'yes' in line.lower()
|
|
elif line.startswith('REASONING:'):
|
|
reasoning = line.split(':', 1)[1].strip()
|
|
break
|
|
|
|
return success, reasoning
|
|
|
|
return False, "Unknown pattern type"
|
|
|
|
def evaluate_indicator(self, indicator: SuccessIndicator, log_content: str) -> EvaluationResult:
|
|
"""Evaluate a success indicator against logs"""
|
|
all_results = []
|
|
|
|
for pattern in indicator.patterns:
|
|
success, reasoning = self.evaluate_pattern(
|
|
pattern, log_content, indicator.context_hint or ""
|
|
)
|
|
all_results.append((success, reasoning))
|
|
|
|
# Aggregate results based on require_all flag
|
|
if indicator.require_all:
|
|
# All patterns must succeed
|
|
overall_success = all(r[0] for r in all_results)
|
|
else:
|
|
# Any pattern succeeds (OR logic)
|
|
overall_success = any(r[0] for r in all_results)
|
|
|
|
# Collect all reasoning
|
|
all_reasoning = []
|
|
for _, reasoning in all_results:
|
|
all_reasoning.append(reasoning)
|
|
|
|
return EvaluationResult(
|
|
indicator_name=indicator.name,
|
|
success=overall_success,
|
|
reasoning=" | ".join(all_reasoning[:3]) # Combine reasoning
|
|
)
|
|
|
|
def evaluate_target(self, target: Target, log_content: str) -> EvaluationReport:
|
|
"""Evaluate entire target against logs"""
|
|
vulnerability_evaluations = []
|
|
|
|
# Evaluate each vulnerability
|
|
for vuln in target.get_all_vulnerabilities():
|
|
results = []
|
|
for indicator in vuln.success_indicators:
|
|
result = self.evaluate_indicator(indicator, log_content)
|
|
results.append(result)
|
|
|
|
vuln_eval = VulnerabilityEvaluation(
|
|
vulnerability=vuln,
|
|
results=results
|
|
)
|
|
vulnerability_evaluations.append(vuln_eval)
|
|
|
|
# Calculate overall success rate
|
|
total_vulns = len(vulnerability_evaluations)
|
|
exploited_vulns = sum(1 for v in vulnerability_evaluations if v.exploited)
|
|
success_rate = exploited_vulns / total_vulns if total_vulns > 0 else 0.0
|
|
|
|
# Check attack chain completion
|
|
chain_status = {}
|
|
for step in target.attack_steps:
|
|
# Step is complete if any vulnerability in it was exploited
|
|
step_vulns = [v for v in vulnerability_evaluations
|
|
if v.vulnerability in step.vulnerabilities]
|
|
chain_status[step.step_id] = any(v.exploited for v in step_vulns)
|
|
|
|
# Generate summary
|
|
summary = self._generate_summary(vulnerability_evaluations, chain_status)
|
|
|
|
return EvaluationReport(
|
|
target_name=target.name,
|
|
vulnerability_evaluations=vulnerability_evaluations,
|
|
overall_success_rate=success_rate,
|
|
attack_chain_completed=chain_status,
|
|
summary=summary
|
|
)
|
|
|
|
def _generate_summary(self, vuln_evals: List[VulnerabilityEvaluation],
|
|
chain_status: Dict[str, bool]) -> str:
|
|
"""Generate a summary of the evaluation"""
|
|
exploited = [v for v in vuln_evals if v.exploited]
|
|
|
|
if not exploited:
|
|
return "No vulnerabilities were successfully exploited."
|
|
|
|
summary_parts = []
|
|
summary_parts.append(f"Successfully exploited {len(exploited)} vulnerabilities.")
|
|
|
|
# Highlight critical findings
|
|
critical = [v for v in exploited if v.vulnerability.difficulty == "critical"]
|
|
if critical:
|
|
summary_parts.append(f"Critical findings: {', '.join(v.vulnerability.vuln_type.value for v in critical)}")
|
|
|
|
# Check if full chain completed
|
|
if all(chain_status.values()):
|
|
summary_parts.append("Complete attack chain achieved!")
|
|
|
|
return " ".join(summary_parts)
|
|
|
|
|
|
# Example usage
|
|
if __name__ == "__main__":
|
|
import os
|
|
import sys
|
|
|
|
# Configuration
|
|
log_file = "logs/PentestGPT-Env-1-Partial"
|
|
|
|
# Check if log file exists
|
|
if not os.path.exists(log_file):
|
|
print(f"Error: Log file '{log_file}' not found.")
|
|
print("Please ensure the log file exists in the logs directory.")
|
|
sys.exit(1)
|
|
|
|
# Read sample log content
|
|
with open(log_file, "r") as f:
|
|
sample_log = f.read()
|
|
|
|
print(f"Loaded log file: {log_file} ({len(sample_log)} characters)")
|
|
|
|
# Import and create target
|
|
from benchmark.pentestTarget import create_thinkphp_target
|
|
target = create_thinkphp_target()
|
|
|
|
# Choose LLM provider
|
|
llm_choice = os.getenv("LLM_PROVIDER", "ollama").lower()
|
|
|
|
if llm_choice == "claude":
|
|
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
if not api_key:
|
|
print("Error: ANTHROPIC_API_KEY environment variable not set for Claude")
|
|
print("Please set: export ANTHROPIC_API_KEY=your_api_key")
|
|
sys.exit(1)
|
|
llm = ClaudeLLM(api_key=api_key)
|
|
print("Using Claude API")
|
|
else:
|
|
# Default to Ollama
|
|
model_name = os.getenv("OLLAMA_MODEL", "llama3.1:latest")
|
|
ollama_url = os.getenv("OLLAMA_URL", "http://localhost:11434")
|
|
llm = OllamaLLM(model_name=model_name, base_url=ollama_url)
|
|
print(f"Using Ollama with model: {model_name}")
|
|
|
|
# Create evaluator
|
|
evaluator = PentestEvaluator(llm)
|
|
|
|
print("\\nStarting evaluation...")
|
|
print("=" * 60)
|
|
|
|
# Evaluate the target
|
|
report = evaluator.evaluate_target(target, sample_log)
|
|
|
|
# Print results
|
|
report.print_summary()
|
|
|
|
# Additional detailed output
|
|
print("\\nDetailed Results:")
|
|
print("-" * 40)
|
|
for vuln_eval in report.vulnerability_evaluations:
|
|
print(f"\\nVulnerability: {vuln_eval.vulnerability.vuln_type.value}")
|
|
print(f"Location: {vuln_eval.vulnerability.location}")
|
|
print(f"Exploited: {'Yes' if vuln_eval.exploited else 'No'}")
|
|
|
|
for result in vuln_eval.results:
|
|
print(f" Indicator: {result.indicator_name}")
|
|
print(f" Success: {result.success}")
|
|
print(f" Reasoning: {result.reasoning[:150]}...")
|
|
print()
|
|
|
|
print("\\nEvaluation completed!")
|
|
print("\\nUsage Tips:")
|
|
print("- Set LLM_PROVIDER=claude to use Claude API")
|
|
print("- Set LLM_PROVIDER=ollama to use Ollama (default)")
|
|
print("- Set ANTHROPIC_API_KEY for Claude")
|
|
print("- Set OLLAMA_MODEL and OLLAMA_URL for Ollama customization") |