393 lines
16 KiB
Python
393 lines
16 KiB
Python
"""Node definitions for Passive Website Vulnerability Assessment."""
|
|
|
|
from framework.graph import NodeSpec
|
|
|
|
# Node 1: Intake (client-facing)
|
|
# Collect the target domain and confirm scanning scope.
|
|
intake_node = NodeSpec(
|
|
id="intake",
|
|
name="Intake",
|
|
description="Collect the target website domain from the user and confirm the scanning scope",
|
|
node_type="event_loop",
|
|
client_facing=True,
|
|
max_node_visits=0,
|
|
input_keys=[],
|
|
output_keys=["target_domain"],
|
|
system_prompt="""\
|
|
You are the intake specialist for a passive website vulnerability assessment agent.
|
|
|
|
**STEP 1 — Greet and collect target (text only, NO tool calls):**
|
|
Ask the user for the website domain they want to assess. If they already provided one, \
|
|
confirm it.
|
|
|
|
Clarify:
|
|
- The exact domain or URL (e.g., example.com, https://app.example.com)
|
|
- Any specific areas of concern (e.g., email security, SSL, exposed services)
|
|
|
|
Explain briefly that this is a **passive, non-intrusive assessment** — we only examine \
|
|
publicly available information (SSL certificates, HTTP headers, DNS records, open ports, \
|
|
tech fingerprints, and public subdomain data). No attack payloads or exploit attempts.
|
|
|
|
Keep it brief. One message, 2-3 questions max.
|
|
|
|
After your message, call ask_user() to wait for the user's response.
|
|
|
|
**STEP 2 — After the user responds, call set_output:**
|
|
- set_output("target_domain", "the confirmed domain/URL to test, e.g. https://example.com")
|
|
""",
|
|
tools=[],
|
|
)
|
|
|
|
# Node 2: Passive Reconnaissance
|
|
# Runs all 6 scanning tools — no CLI dependencies, no attack payloads.
|
|
passive_recon_node = NodeSpec(
|
|
id="passive-recon",
|
|
name="Passive Reconnaissance",
|
|
description=(
|
|
"Run all 6 passive scanning tools against the target domain: SSL/TLS, "
|
|
"HTTP headers, DNS security, port scanning, tech stack detection, and "
|
|
"subdomain enumeration"
|
|
),
|
|
node_type="event_loop",
|
|
max_node_visits=0,
|
|
input_keys=["target_domain", "feedback"],
|
|
output_keys=["scan_results"],
|
|
system_prompt="""\
|
|
You are a passive reconnaissance specialist. Given a target domain, run all 6 scanning \
|
|
tools to assess the security posture. These tools are non-intrusive and OSINT-based.
|
|
|
|
If feedback is provided (not None/empty), this is a follow-up round — focus on the areas \
|
|
the user requested. You may skip tools that aren't relevant to the feedback. If feedback \
|
|
is None or empty, this is the first scan — run ALL 6 tools.
|
|
|
|
**Run these tools against the target domain:**
|
|
|
|
1. **ssl_tls_scan(hostname)** — Checks TLS version, certificate validity, cipher strength
|
|
2. **http_headers_scan(url)** — Checks OWASP-recommended security headers (HSTS, CSP, \
|
|
X-Frame-Options, etc.)
|
|
3. **dns_security_scan(domain)** — Checks SPF, DMARC, DKIM, DNSSEC, zone transfer
|
|
4. **port_scan(hostname)** — TCP connect scan on top 20 common ports, flags exposed \
|
|
database/admin ports
|
|
5. **tech_stack_detect(url)** — Detects web server, framework, CMS, JS libraries, cookies
|
|
6. **subdomain_enumerate(domain)** — Queries Certificate Transparency logs for subdomains
|
|
|
|
**IMPORTANT:**
|
|
- Extract just the hostname/domain from the URL for tools that need it \
|
|
(e.g., "example.com" not "https://example.com")
|
|
- Use the full URL (with https://) for http_headers_scan and tech_stack_detect
|
|
- Run tools in batches of 2-3 to avoid overwhelming the system
|
|
- If a tool fails, note the error and continue with the remaining tools
|
|
|
|
**After all tools complete, compile results:**
|
|
|
|
Combine ALL tool outputs into a single JSON object and store it:
|
|
|
|
set_output("scan_results", "<JSON string containing all 6 tool results: \
|
|
{ssl: {...}, headers: {...}, dns: {...}, ports: {...}, tech: {...}, subdomains: {...}}>")
|
|
|
|
Each tool returns a grade_input dict — preserve these as-is, the risk scorer needs them.
|
|
""",
|
|
tools=[
|
|
"ssl_tls_scan",
|
|
"http_headers_scan",
|
|
"dns_security_scan",
|
|
"port_scan",
|
|
"tech_stack_detect",
|
|
"subdomain_enumerate",
|
|
],
|
|
)
|
|
|
|
# Node 3: Risk Scoring
|
|
# Calculates weighted letter grades from scan results.
|
|
risk_scoring_node = NodeSpec(
|
|
id="risk-scoring",
|
|
name="Risk Scoring",
|
|
description=(
|
|
"Calculate weighted letter grades (A-F) per security category and overall "
|
|
"risk score from scan results"
|
|
),
|
|
node_type="event_loop",
|
|
max_node_visits=0,
|
|
input_keys=["scan_results"],
|
|
output_keys=["risk_report"],
|
|
system_prompt="""\
|
|
You calculate risk scores from scan results.
|
|
|
|
Given scan_results (a JSON string with ssl, headers, dns, ports, tech, subdomains \
|
|
sections), call the risk_score tool to produce letter grades.
|
|
|
|
**Step 1 — Extract scan results and call risk_score:**
|
|
|
|
The risk_score tool accepts JSON strings for each category. Extract the relevant \
|
|
sections from scan_results and pass them:
|
|
|
|
risk_score(
|
|
ssl_results="<JSON string of the ssl section from scan_results>",
|
|
headers_results="<JSON string of the headers section from scan_results>",
|
|
dns_results="<JSON string of the dns section from scan_results>",
|
|
ports_results="<JSON string of the ports section from scan_results>",
|
|
tech_results="<JSON string of the tech section from scan_results>",
|
|
subdomain_results="<JSON string of the subdomains section from scan_results>"
|
|
)
|
|
|
|
If a category has no results (tool failed), pass an empty string for that parameter.
|
|
|
|
**Step 2 — Store the risk report:**
|
|
|
|
set_output("risk_report", "<the complete JSON output from risk_score, including \
|
|
overall_score, overall_grade, categories, top_risks, and grade_scale>")
|
|
""",
|
|
tools=["risk_score"],
|
|
)
|
|
|
|
# Node 4: Findings Review (client-facing)
|
|
# Present risk grades and ask the user to continue or generate report.
|
|
findings_review_node = NodeSpec(
|
|
id="findings-review",
|
|
name="Findings Review",
|
|
description=(
|
|
"Present risk grades and security findings to the user, ask whether to "
|
|
"continue deeper scanning or generate the final report"
|
|
),
|
|
node_type="event_loop",
|
|
client_facing=True,
|
|
max_node_visits=0,
|
|
input_keys=["scan_results", "risk_report", "target_domain"],
|
|
output_keys=["continue_scanning", "feedback", "all_findings"],
|
|
system_prompt="""\
|
|
You present security scan findings and risk grades to the user and ask for their decision.
|
|
|
|
**STEP 1 — Present findings (text only, NO tool calls):**
|
|
|
|
Display the results in this format:
|
|
|
|
1. **Overall Risk Grade** — Show the letter grade prominently \
|
|
(e.g., "Overall Grade: C (68/100)")
|
|
|
|
2. **Category Breakdown** — Table showing each category's grade:
|
|
| Category | Grade | Score | Findings |
|
|
|----------|-------|-------|----------|
|
|
| SSL/TLS | B | 85 | 1 issue |
|
|
| HTTP Headers | D | 45 | 4 issues |
|
|
| DNS Security | C | 60 | 3 issues |
|
|
| Network Exposure | C | 70 | 1 issue |
|
|
| Technology | B | 75 | 2 issues |
|
|
| Attack Surface | B | 80 | 1 issue |
|
|
|
|
3. **Top Risks** — List the most critical findings from the risk report's top_risks field
|
|
|
|
4. **Grade Scale** — Show the grade scale so the user understands the scoring:
|
|
- A (90-100): Excellent security posture
|
|
- B (75-89): Good, minor improvements needed
|
|
- C (60-74): Fair, notable security gaps
|
|
- D (40-59): Poor, significant vulnerabilities
|
|
- F (0-39): Critical, immediate action required
|
|
|
|
5. **Options** — Ask: "Would you like me to:
|
|
- **Continue scanning** — I can focus on specific weak areas for a deeper look
|
|
- **Generate the report** — I'll compile a full HTML risk dashboard with all \
|
|
findings and remediation steps"
|
|
|
|
After your message, call ask_user() to wait for the user's response.
|
|
|
|
**STEP 2 — After the user responds, call set_output:**
|
|
|
|
If the user wants to continue:
|
|
- set_output("continue_scanning", "true")
|
|
- set_output("feedback", "What the user wants investigated further, or \
|
|
'focus on weakest categories'")
|
|
- set_output("all_findings", "Accumulated findings from all rounds so far as JSON string")
|
|
|
|
If the user wants to stop and get the report:
|
|
- set_output("continue_scanning", "false")
|
|
- set_output("feedback", "")
|
|
- set_output("all_findings", "All scan results and risk report combined as JSON string")
|
|
""",
|
|
tools=[],
|
|
)
|
|
|
|
# Node 5: Final Report (client-facing)
|
|
# Generates an HTML risk dashboard with color-coded grades.
|
|
final_report_node = NodeSpec(
|
|
id="final-report",
|
|
name="Risk Dashboard Report",
|
|
description=(
|
|
"Generate an HTML risk dashboard with color-coded grades, category breakdown, "
|
|
"detailed findings, and remediation steps"
|
|
),
|
|
node_type="event_loop",
|
|
client_facing=True,
|
|
max_node_visits=0,
|
|
input_keys=["all_findings", "risk_report", "target_domain"],
|
|
output_keys=["report_status"],
|
|
system_prompt="""\
|
|
Generate an HTML risk dashboard report and deliver it to the user.
|
|
|
|
**CRITICAL: You MUST build the file in multiple append_data calls. NEVER try to write the \
|
|
entire HTML in a single save_data call — it will exceed the output token limit and fail.**
|
|
|
|
**PROCESS (follow exactly):**
|
|
|
|
**Step 1 — Write HTML head + header + overall grade (save_data):**
|
|
Call save_data to create the file with the HTML head, full CSS, header, overall grade \
|
|
circle, and grade scale legend.
|
|
```
|
|
save_data(filename="risk_assessment_report.html", data="<!DOCTYPE html>\\n<html>...")
|
|
```
|
|
|
|
Include: DOCTYPE, head with ALL styles below, opening body, header with target domain \
|
|
and scan date, overall grade circle with score, and the grade scale legend table.
|
|
|
|
**CSS to use (copy exactly):**
|
|
```
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#f5f7fa;\
|
|
color:#333;line-height:1.6}
|
|
header{background:linear-gradient(135deg,#1e3c72 0%,#2a5298 100%);color:white;\
|
|
padding:40px 20px;text-align:center}
|
|
header h1{font-size:2.5em;margin-bottom:10px}
|
|
header p{font-size:1.1em;opacity:0.9}
|
|
.container{max-width:1200px;margin:40px auto;padding:0 20px}
|
|
h2{color:#1e3c72;border-bottom:2px solid #2a5298;padding-bottom:10px;margin-top:30px}
|
|
h3{color:#2a5298;margin-top:20px}
|
|
.grade-display{text-align:center;margin:40px 0;background:white;padding:40px;\
|
|
border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,0.1)}
|
|
.grade-circle{width:120px;height:120px;border-radius:50%;display:flex;\
|
|
align-items:center;justify-content:center;margin:0 auto 20px;font-size:3em;\
|
|
font-weight:bold;color:white}
|
|
.grade-a{background:#27ae60} .grade-b{background:#3498db}
|
|
.grade-c{background:#f39c12} .grade-d{background:#e74c3c}
|
|
.grade-f{background:#c0392b}
|
|
.category-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));\
|
|
gap:20px;margin:40px 0}
|
|
.category-card{background:white;padding:25px;border-radius:10px;\
|
|
box-shadow:0 2px 10px rgba(0,0,0,0.1);border-left:5px solid #ccc}
|
|
.category-card.a{border-left-color:#27ae60} .category-card.b{border-left-color:#3498db}
|
|
.category-card.c{border-left-color:#f39c12} .category-card.d{border-left-color:#e74c3c}
|
|
.category-card.f{border-left-color:#c0392b}
|
|
.badge{display:inline-block;padding:4px 10px;border-radius:12px;color:white;\
|
|
font-weight:bold;font-size:0.85em}
|
|
.badge.high{background:#c0392b} .badge.medium{background:#f39c12}
|
|
.badge.low{background:#3498db} .badge.info{background:#95a5a6}
|
|
.finding{margin:20px 0;padding:20px;background:#f9f9f9;border-left:4px solid #ccc;\
|
|
border-radius:5px}
|
|
.finding.high{border-left-color:#c0392b} .finding.medium{border-left-color:#f39c12}
|
|
.finding.low{border-left-color:#3498db} .finding.info{border-left-color:#95a5a6}
|
|
.remediation{margin-top:15px;padding:15px;background:white;border-radius:5px;\
|
|
border-left:3px solid #27ae60}
|
|
.remediation h5{color:#27ae60;margin-bottom:10px}
|
|
pre{background:#2c3e50;color:#ecf0f1;padding:15px;border-radius:5px;overflow-x:auto;\
|
|
margin:10px 0;font-family:'Courier New',monospace;font-size:0.9em}
|
|
.card{background:white;border-radius:10px;padding:25px;margin:20px 0;\
|
|
box-shadow:0 2px 10px rgba(0,0,0,0.1)}
|
|
.footer{text-align:center;padding:30px 20px;color:#666;border-top:1px solid #ddd;\
|
|
margin-top:50px}
|
|
.grade-scale{background:white;padding:25px;border-radius:10px;margin:30px 0}
|
|
.grade-scale-item{padding:10px 0;border-bottom:1px solid #eee}
|
|
@media(max-width:768px){.category-grid{grid-template-columns:1fr}\
|
|
header h1{font-size:1.8em}.grade-circle{width:80px;height:80px;font-size:2em}}
|
|
```
|
|
|
|
**Grade circle HTML pattern:**
|
|
```
|
|
<div class="grade-display">
|
|
<div class="grade-circle grade-{letter}">{LETTER}</div>
|
|
<p style="font-size:1.8em;margin:20px 0">Overall Score: {score}/100</p>
|
|
<p style="color:#666">{one-line assessment}</p>
|
|
</div>
|
|
```
|
|
|
|
**Grade scale legend pattern:**
|
|
```
|
|
<div class="grade-scale">
|
|
<h3>Grade Scale</h3>
|
|
<div class="grade-scale-item"><strong>A (90-100):</strong> Excellent</div>
|
|
<div class="grade-scale-item"><strong>B (75-89):</strong> Good</div>
|
|
<div class="grade-scale-item"><strong>C (60-74):</strong> Fair</div>
|
|
<div class="grade-scale-item"><strong>D (40-59):</strong> Poor</div>
|
|
<div class="grade-scale-item"><strong>F (0-39):</strong> Critical</div>
|
|
</div>
|
|
```
|
|
|
|
End Step 1 after the grade scale closing div. Do NOT close body/html yet.
|
|
|
|
**Step 2 — Append category breakdown grid (append_data):**
|
|
```
|
|
append_data(filename="risk_assessment_report.html", data="<h2>Category Breakdown</h2>...")
|
|
```
|
|
|
|
Use this pattern for each of the 6 category cards:
|
|
```
|
|
<div class="category-card {letter}">
|
|
<h3>{Category Name}</h3>
|
|
<p><span class="badge {letter_class}">Grade: {LETTER} ({score})</span></p>
|
|
<p>{findings_count} findings</p>
|
|
<p style="color:#666;font-size:0.95em">{one-line summary}</p>
|
|
</div>
|
|
```
|
|
|
|
Wrap all 6 cards in `<div class="category-grid">...</div>`. Close the grid div.
|
|
|
|
**Step 3 — Append detailed findings PER CATEGORY (one append_data per category):**
|
|
For EACH of the 6 categories that has findings, call append_data separately:
|
|
```
|
|
append_data(filename="risk_assessment_report.html", data="<h3>{Category Name} (Grade: {LETTER})</h3>...")
|
|
```
|
|
|
|
Skip categories with 0 findings. For each finding, use this exact pattern:
|
|
```
|
|
<div class="finding {severity}">
|
|
<h4>{Title} <span class="badge {severity}">{SEVERITY}</span></h4>
|
|
<p><strong>Impact:</strong> {why it matters}</p>
|
|
<div class="remediation">
|
|
<h5>How to Fix</h5>
|
|
<p>{step-by-step instructions}</p>
|
|
<pre>{code example if relevant}</pre>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
Where {severity} is one of: high, medium, low, info.
|
|
|
|
**Step 4 — Append footer section (append_data):**
|
|
```
|
|
append_data(filename="risk_assessment_report.html", data="<h2>Top Risks</h2>...")
|
|
```
|
|
|
|
Include:
|
|
- Top Risks: prioritized action items as a numbered list
|
|
- Methodology: "This assessment used passive, OSINT-based scanning..."
|
|
- Disclaimer in a card: "This is an automated passive assessment, not a comprehensive \
|
|
penetration test..."
|
|
- Close with `</div></body></html>`
|
|
|
|
**Step 5 — Serve the file:**
|
|
Call serve_file_to_user(filename="risk_assessment_report.html", open_in_browser=true)
|
|
Print the file_path from the result so the user can click it later.
|
|
|
|
**Step 6 — Present to user (text only, NO tool calls):**
|
|
Summarize: overall grade, weakest category, top 3 action items. \
|
|
After presenting, call ask_user() for follow-ups.
|
|
|
|
**Step 7 — After the user responds:**
|
|
- Answer any questions about findings or remediation
|
|
- Call ask_user() again if they have more questions
|
|
- When the user is satisfied: set_output("report_status", "completed")
|
|
|
|
**IMPORTANT:**
|
|
- Every finding MUST have remediation steps
|
|
- Write for developers, not security experts
|
|
- ALWAYS print the full file path so users can easily access the file later
|
|
- If an append_data call fails with a truncation error, break that chunk into smaller pieces
|
|
""",
|
|
tools=["save_data", "append_data", "serve_file_to_user"],
|
|
)
|
|
|
|
__all__ = [
|
|
"intake_node",
|
|
"passive_recon_node",
|
|
"risk_scoring_node",
|
|
"findings_review_node",
|
|
"final_report_node",
|
|
]
|