Skip to content

Commit ae93d80

Browse files
committed
feat: expand bug bounty template coverage
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
1 parent 7cc94b2 commit ae93d80

8 files changed

Lines changed: 1679 additions & 71 deletions

backend/scripts/seed-templates/exposure-to-cve-brief.json

Lines changed: 441 additions & 0 deletions
Large diffs are not rendered by default.

backend/scripts/seed-templates/public-repo-full-code-security-discord-report.json

Lines changed: 697 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
{
2+
"_metadata": {
3+
"name": "WAF Edge Recon Triage",
4+
"description": "Authorized bug bounty workflow that probes live URLs with HTTPx, detects Web Application Firewalls with wafw00f, and writes a prioritized edge-defense recon report.",
5+
"category": "bug-bounty",
6+
"tags": ["bug-bounty", "recon", "httpx", "wafw00f", "waf", "edge-defense", "triage"],
7+
"author": "sentris-team",
8+
"version": "1.0.0"
9+
},
10+
"manifest": {
11+
"name": "WAF Edge Recon Triage",
12+
"description": "Fingerprint live web assets and detect WAF presence for authorized bug bounty targets.",
13+
"version": "1.0.0",
14+
"author": "sentris-team",
15+
"category": "bug-bounty",
16+
"tags": ["bug-bounty", "recon", "httpx", "wafw00f", "waf", "edge-defense", "triage"],
17+
"entryPoint": "trigger_1",
18+
"nodeCount": 5,
19+
"edgeCount": 8
20+
},
21+
"graph": {
22+
"name": "WAF Edge Recon Triage",
23+
"description": "HTTP probing plus WAF detection for authorized live URLs.",
24+
"nodes": [
25+
{
26+
"id": "trigger_1",
27+
"type": "core.workflow.entrypoint",
28+
"position": { "x": 100, "y": 260 },
29+
"data": {
30+
"label": "Authorized Live URL Input",
31+
"config": {
32+
"params": {
33+
"runtimeInputs": [
34+
{
35+
"id": "liveUrls",
36+
"label": "Live URLs",
37+
"type": "array",
38+
"required": true,
39+
"description": "Authorized HTTP or HTTPS URLs to probe for WAF and service fingerprinting."
40+
},
41+
{
42+
"id": "authorizationNotes",
43+
"label": "Authorization notes",
44+
"type": "text",
45+
"required": false,
46+
"defaultValue": "",
47+
"description": "Program scope, exclusions, and rate-limit notes."
48+
}
49+
]
50+
},
51+
"inputOverrides": {}
52+
}
53+
}
54+
},
55+
{
56+
"id": "httpx_probe",
57+
"type": "sentris.httpx.scan",
58+
"position": { "x": 420, "y": 260 },
59+
"data": {
60+
"label": "Probe Live HTTP Services",
61+
"config": {
62+
"params": {
63+
"threads": 50,
64+
"followRedirects": true,
65+
"tlsProbe": true,
66+
"preferHttps": true
67+
},
68+
"inputOverrides": {}
69+
}
70+
}
71+
},
72+
{
73+
"id": "wafw00f_scan",
74+
"type": "sentris.wafw00f.run",
75+
"position": { "x": 740, "y": 260 },
76+
"data": {
77+
"label": "Detect Web Application Firewalls",
78+
"config": {
79+
"params": { "findAll": false, "verbose": false },
80+
"inputOverrides": {}
81+
}
82+
}
83+
},
84+
{
85+
"id": "rank_waf_recon",
86+
"type": "core.logic.script",
87+
"position": { "x": 1060, "y": 260 },
88+
"data": {
89+
"label": "Rank WAF and HTTP Signals",
90+
"config": {
91+
"params": {
92+
"variables": [
93+
{ "name": "liveUrls", "type": "list-text" },
94+
{ "name": "authorizationNotes", "type": "string" },
95+
{ "name": "httpResponses", "type": "list-json" },
96+
{ "name": "wafDetections", "type": "list-json" },
97+
{ "name": "detectionCount", "type": "number" }
98+
],
99+
"returns": [{ "name": "report", "type": "json" }],
100+
"code": "export function script(input) {\n const liveUrls = Array.isArray(input.liveUrls) ? input.liveUrls : [];\n const httpResponses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\n const wafDetections = Array.isArray(input.wafDetections) ? input.wafDetections : [];\n const authorizationNotes = typeof input.authorizationNotes === 'string' && input.authorizationNotes.trim().length > 0 ? input.authorizationNotes.trim() : null;\n const wafByUrl = new Map();\n for (const item of wafDetections) {\n const url = typeof item?.url === 'string' ? item.url : '';\n if (url) wafByUrl.set(url, item);\n }\n const assets = httpResponses.map((resp) => {\n const url = resp?.url || resp?.finalUrl || resp?.input || '';\n const waf = wafByUrl.get(url) || null;\n return {\n url,\n statusCode: resp?.statusCode ?? null,\n title: resp?.title ?? null,\n technologies: Array.isArray(resp?.technologies) ? resp.technologies : [],\n wafDetected: waf?.detected === true,\n firewall: waf?.firewall || null,\n manufacturer: waf?.manufacturer || null\n };\n }).sort((a, b) => Number(b.wafDetected) - Number(a.wafDetected) || Number(b.technologies?.length || 0) - Number(a.technologies?.length || 0));\n const wafBrands = Array.from(new Set(assets.filter((item) => item.wafDetected).map((item) => item.firewall).filter(Boolean)));\n return {\n report: {\n summary: {\n liveUrls: liveUrls.length,\n probedEndpoints: httpResponses.length,\n wafDetections: Number(input.detectionCount || assets.filter((item) => item.wafDetected).length),\n wafBrands,\n interestingWithoutWaf: assets.filter((item) => !item.wafDetected && [200, 401, 403, 500].includes(Number(item.statusCode))).length\n },\n authorizationNotes,\n assets: assets.slice(0, 100),\n nextSteps: [\n 'Treat WAF-detected endpoints as edge-protected before active testing',\n 'Review 401/403 endpoints without WAF signals for auth bypass opportunities',\n 'Stay within authorized scope and rate limits for follow-up testing'\n ]\n }\n };\n}"
101+
},
102+
"inputOverrides": {}
103+
}
104+
}
105+
},
106+
{
107+
"id": "artifact_report",
108+
"type": "core.artifact.writer",
109+
"position": { "x": 1380, "y": 260 },
110+
"data": {
111+
"label": "Save WAF Recon Report",
112+
"config": {
113+
"params": {
114+
"fileExtension": ".json",
115+
"mimeType": "application/json",
116+
"saveToRunArtifacts": true,
117+
"publishToArtifactLibrary": false
118+
},
119+
"inputOverrides": { "artifactName": "waf-edge-recon-triage-{{date}}" }
120+
}
121+
}
122+
}
123+
],
124+
"edges": [
125+
{
126+
"id": "trigger_1-httpx_probe-targets",
127+
"source": "trigger_1",
128+
"target": "httpx_probe",
129+
"sourceHandle": "liveUrls",
130+
"targetHandle": "targets"
131+
},
132+
{
133+
"id": "trigger_1-wafw00f_scan-targets",
134+
"source": "trigger_1",
135+
"target": "wafw00f_scan",
136+
"sourceHandle": "liveUrls",
137+
"targetHandle": "targets"
138+
},
139+
{
140+
"id": "trigger_1-rank_waf_recon-liveUrls",
141+
"source": "trigger_1",
142+
"target": "rank_waf_recon",
143+
"sourceHandle": "liveUrls",
144+
"targetHandle": "liveUrls"
145+
},
146+
{
147+
"id": "trigger_1-rank_waf_recon-authorizationNotes",
148+
"source": "trigger_1",
149+
"target": "rank_waf_recon",
150+
"sourceHandle": "authorizationNotes",
151+
"targetHandle": "authorizationNotes"
152+
},
153+
{
154+
"id": "httpx_probe-rank_waf_recon-responses",
155+
"source": "httpx_probe",
156+
"target": "rank_waf_recon",
157+
"sourceHandle": "responses",
158+
"targetHandle": "httpResponses"
159+
},
160+
{
161+
"id": "wafw00f_scan-rank_waf_recon-detections",
162+
"source": "wafw00f_scan",
163+
"target": "rank_waf_recon",
164+
"sourceHandle": "wafDetections",
165+
"targetHandle": "wafDetections"
166+
},
167+
{
168+
"id": "wafw00f_scan-rank_waf_recon-detectionCount",
169+
"source": "wafw00f_scan",
170+
"target": "rank_waf_recon",
171+
"sourceHandle": "detectionCount",
172+
"targetHandle": "detectionCount"
173+
},
174+
{
175+
"id": "rank_waf_recon-artifact_report-report",
176+
"source": "rank_waf_recon",
177+
"target": "artifact_report",
178+
"sourceHandle": "report",
179+
"targetHandle": "content"
180+
}
181+
]
182+
},
183+
"requiredSecrets": []
184+
}

backend/src/templates/__tests__/seed-templates.spec.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,21 @@ const newTemplateFiles = [
1313
'container-image-cve-triage.json',
1414
'cve-impact-research-brief.json',
1515
'exposed-service-cve-mapper.json',
16+
'exposure-to-cve-brief.json',
1617
'github-dependency-cve-hunt-discord-report.json',
1718
'github-repo-dependency-cve-triage.json',
1819
'kev-fresh-cve-watch-brief.json',
1920
'npm-dependency-cve-hunt.json',
2021
'passive-osint-subdomain-expansion.json',
2122
'public-repo-code-iac-risk-triage.json',
23+
'public-repo-full-code-security-discord-report.json',
2224
'public-repo-full-code-security.json',
2325
'public-repo-secret-exposure-triage.json',
2426
'security-scan-discord-report.json',
2527
'subdomain-takeover-triage.json',
2628
'tech-stack-cve-hunter.json',
2729
'web-api-fuzz-triage.json',
30+
'wafw00f-edge-recon-triage.json',
2831
'web-attack-surface-quick-win-hunt.json',
2932
];
3033

@@ -2037,4 +2040,145 @@ describe('new seed templates', () => {
20372040
expect(result.report.assets[0].url).toBe('https://www.example.com');
20382041
expect(result.report.assets[0].statusCode).toBe(200);
20392042
});
2043+
2044+
it('public-repo-full-code-security-discord-report wires Run Report Discord after artifact save', () => {
2045+
const filePath = join(seedTemplatesDir, 'public-repo-full-code-security-discord-report.json');
2046+
const template = JSON.parse(readFileSync(filePath, 'utf8'));
2047+
2048+
expect(template.manifest.nodeCount).toBe(14);
2049+
expect(template.requiredSecrets).toEqual([
2050+
expect.objectContaining({ name: 'DISCORD_WEBHOOK_URL', type: 'string' }),
2051+
]);
2052+
expect(
2053+
template.graph.edges.find(
2054+
(edge: { id: string }) => edge.id === 'artifact_report-run_report_discord-after',
2055+
),
2056+
).toEqual(
2057+
expect.objectContaining({
2058+
source: 'artifact_report',
2059+
target: 'run_report_discord',
2060+
sourceHandle: 'saved',
2061+
targetHandle: 'after',
2062+
}),
2063+
);
2064+
});
2065+
2066+
it('exposure-to-cve-brief chains exposure mapping into CVE impact brief assembly', () => {
2067+
const filePath = join(seedTemplatesDir, 'exposure-to-cve-brief.json');
2068+
const template = JSON.parse(readFileSync(filePath, 'utf8'));
2069+
const nodeTypes = template.graph.nodes.map((node: { type: string }) => node.type);
2070+
2071+
expect(nodeTypes).toEqual(
2072+
expect.arrayContaining([
2073+
'sentris.naabu.scan',
2074+
'sentris.httpx.scan',
2075+
'sentris.nvd.cve.query',
2076+
'core.http.request',
2077+
'core.logic.script',
2078+
'core.artifact.writer',
2079+
]),
2080+
);
2081+
expect(
2082+
template.graph.edges.find(
2083+
(edge: { id: string }) => edge.id === 'rank_cve_candidates-pick_top_cve-report',
2084+
),
2085+
).toBeTruthy();
2086+
expect(
2087+
template.graph.edges.find(
2088+
(edge: { id: string }) => edge.id === 'pick_top_cve-query_nvd_detail-cveId',
2089+
),
2090+
).toBeTruthy();
2091+
expect(
2092+
template.graph.edges.find(
2093+
(edge: { id: string }) => edge.id === 'pick_top_cve-query_nvd_detail-lookupKeyword',
2094+
),
2095+
).toBeTruthy();
2096+
});
2097+
2098+
it('exposure-to-cve-brief pick script selects the top ranked CVE candidate', () => {
2099+
const filePath = join(seedTemplatesDir, 'exposure-to-cve-brief.json');
2100+
const template = JSON.parse(readFileSync(filePath, 'utf8'));
2101+
const pickNode = template.graph.nodes.find(
2102+
(node: { id: string }) => node.id === 'pick_top_cve',
2103+
);
2104+
const result = runTemplateScript<{ cveId: string; product: string; cveLookupKeyword: string }>(
2105+
pickNode.data.config.params.code,
2106+
{
2107+
report: {
2108+
summary: { topCandidate: 'CVE-2024-0001', fingerprintKeyword: 'nginx' },
2109+
candidates: [{ id: 'CVE-2024-0001' }, { id: 'CVE-2023-9999' }],
2110+
},
2111+
},
2112+
);
2113+
2114+
expect(result.cveId).toBe('CVE-2024-0001');
2115+
expect(result.product).toBe('nginx');
2116+
expect(result.cveLookupKeyword).toBe('CVE-2024-0001');
2117+
});
2118+
2119+
it('exposure-to-cve-brief pick script falls back to product keyword when no CVE is ranked', () => {
2120+
const filePath = join(seedTemplatesDir, 'exposure-to-cve-brief.json');
2121+
const template = JSON.parse(readFileSync(filePath, 'utf8'));
2122+
const pickNode = template.graph.nodes.find(
2123+
(node: { id: string }) => node.id === 'pick_top_cve',
2124+
);
2125+
const result = runTemplateScript<{ cveId: string; product: string; cveLookupKeyword: string }>(
2126+
pickNode.data.config.params.code,
2127+
{
2128+
report: {
2129+
summary: { topCandidate: null, fingerprintKeyword: 'nginx' },
2130+
candidates: [],
2131+
},
2132+
},
2133+
);
2134+
2135+
expect(result.cveId).toBe('');
2136+
expect(result.product).toBe('nginx');
2137+
expect(result.cveLookupKeyword).toBe('nginx');
2138+
});
2139+
2140+
it('wafw00f-edge-recon-triage wires httpx and wafw00f into a ranked report', () => {
2141+
const filePath = join(seedTemplatesDir, 'wafw00f-edge-recon-triage.json');
2142+
const template = JSON.parse(readFileSync(filePath, 'utf8'));
2143+
2144+
expect(template.graph.nodes.map((node: { type: string }) => node.type)).toEqual([
2145+
'core.workflow.entrypoint',
2146+
'sentris.httpx.scan',
2147+
'sentris.wafw00f.run',
2148+
'core.logic.script',
2149+
'core.artifact.writer',
2150+
]);
2151+
});
2152+
2153+
it('wafw00f-edge-recon-triage rank script merges HTTP and WAF detections by URL', () => {
2154+
const filePath = join(seedTemplatesDir, 'wafw00f-edge-recon-triage.json');
2155+
const template = JSON.parse(readFileSync(filePath, 'utf8'));
2156+
const rankNode = template.graph.nodes.find(
2157+
(node: { id: string }) => node.id === 'rank_waf_recon',
2158+
);
2159+
const result = runTemplateScript<{
2160+
report: {
2161+
summary: Record<string, unknown>;
2162+
assets: { url: string; wafDetected: boolean; firewall?: string }[];
2163+
};
2164+
}>(rankNode.data.config.params.code, {
2165+
liveUrls: ['https://app.example.com'],
2166+
httpResponses: [
2167+
{ url: 'https://app.example.com', statusCode: 200, title: 'App', technologies: ['nginx'] },
2168+
],
2169+
wafDetections: [
2170+
{
2171+
url: 'https://app.example.com',
2172+
detected: true,
2173+
firewall: 'Cloudflare',
2174+
manufacturer: 'Cloudflare',
2175+
},
2176+
],
2177+
detectionCount: 1,
2178+
});
2179+
2180+
expect(result.report.summary.wafDetections).toBe(1);
2181+
expect(result.report.assets[0].wafDetected).toBe(true);
2182+
expect(result.report.assets[0].firewall).toBe('Cloudflare');
2183+
});
20402184
});

backend/src/templates/__tests__/templates.service.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,18 @@ const securityTemplateFiles = [
5858
'bug-bounty-recon-triage.json',
5959
'cve-impact-research-brief.json',
6060
'exposed-service-cve-mapper.json',
61+
'exposure-to-cve-brief.json',
6162
'github-dependency-cve-hunt-discord-report.json',
6263
'github-repo-dependency-cve-triage.json',
6364
'kev-fresh-cve-watch-brief.json',
6465
'npm-dependency-cve-hunt.json',
66+
'public-repo-full-code-security-discord-report.json',
6567
'public-repo-full-code-security.json',
6668
'public-repo-secret-exposure-triage.json',
6769
'security-scan-discord-report.json',
6870
'subdomain-takeover-triage.json',
6971
'tech-stack-cve-hunter.json',
72+
'wafw00f-edge-recon-triage.json',
7073
'web-attack-surface-quick-win-hunt.json',
7174
] as const;
7275

0 commit comments

Comments
 (0)