diff --git a/frontend/src/pages/__tests__/ActionCenterPage.test.tsx b/frontend/src/pages/__tests__/ActionCenterPage.test.tsx
index 9a151f67..edc3f30d 100644
--- a/frontend/src/pages/__tests__/ActionCenterPage.test.tsx
+++ b/frontend/src/pages/__tests__/ActionCenterPage.test.tsx
@@ -148,11 +148,14 @@ describe('ActionCenterPage', () => {
]),
);
- it('renders page heading', () => {
+ it('omits the redundant page heading supplied by the app top bar', () => {
setupStore();
renderPage();
- expect(screen.getByRole('heading', { level: 2, name: /Action Center/i })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('heading', { level: 2, name: /^Action Center$/ }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByPlaceholderText(/Filter by title, node, or run ID/i)).toBeInTheDocument();
});
it('renders loading skeletons when isLoading is true and no data', () => {
diff --git a/frontend/src/pages/__tests__/ArtifactLibrary.test.tsx b/frontend/src/pages/__tests__/ArtifactLibrary.test.tsx
index 156c45eb..5e80d0cf 100644
--- a/frontend/src/pages/__tests__/ArtifactLibrary.test.tsx
+++ b/frontend/src/pages/__tests__/ArtifactLibrary.test.tsx
@@ -198,10 +198,13 @@ describe('ArtifactLibrary', () => {
expect(screen.getByPlaceholderText('Filter by name...')).toBeInTheDocument();
});
- it('renders page heading', () => {
+ it('omits the redundant page heading supplied by the app top bar', () => {
renderPage();
- expect(screen.getByRole('heading', { level: 2, name: /Artifacts/i })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('heading', { level: 2, name: /^Artifacts$/ }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByPlaceholderText('Filter by name...')).toBeInTheDocument();
});
it('renders loading skeletons when isLoading is true', () => {
diff --git a/frontend/src/pages/__tests__/DashboardPage.test.tsx b/frontend/src/pages/__tests__/DashboardPage.test.tsx
index 843bf1f5..4f940f6b 100644
--- a/frontend/src/pages/__tests__/DashboardPage.test.tsx
+++ b/frontend/src/pages/__tests__/DashboardPage.test.tsx
@@ -131,10 +131,13 @@ describe('DashboardPage', () => {
// Loading
describe('loading state', () => {
- it('renders heading during loading', () => {
+ it('omits the redundant page heading supplied by the app top bar during loading', () => {
setup({ isLoading: true });
renderPage();
- expect(screen.getByRole('heading', { name: 'Dashboard', level: 2 })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('heading', { name: 'Dashboard', level: 2 }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText('Workflows')).toBeInTheDocument();
});
it('renders skeleton placeholders when isLoading', () => {
diff --git a/frontend/src/pages/__tests__/FindingsPage.test.tsx b/frontend/src/pages/__tests__/FindingsPage.test.tsx
index 369cb455..7dbd84c7 100644
--- a/frontend/src/pages/__tests__/FindingsPage.test.tsx
+++ b/frontend/src/pages/__tests__/FindingsPage.test.tsx
@@ -99,10 +99,12 @@ describe('FindingsPage', () => {
cleanup();
});
- it('renders page heading', () => {
+ it('omits the redundant page heading supplied by the app top bar', () => {
setupStore();
renderPage();
- expect(screen.getByRole('heading', { level: 2, name: /Findings/i })).toBeInTheDocument();
+
+ expect(screen.queryByRole('heading', { level: 2, name: /^Findings$/ })).not.toBeInTheDocument();
+ expect(screen.getByPlaceholderText(/Search findings by name/i)).toBeInTheDocument();
});
it('renders loading skeletons when isLoading is true and no data', () => {
diff --git a/frontend/src/pages/__tests__/IntegrationsManager.test.tsx b/frontend/src/pages/__tests__/IntegrationsManager.test.tsx
index a14f1545..5ffd7f8e 100644
--- a/frontend/src/pages/__tests__/IntegrationsManager.test.tsx
+++ b/frontend/src/pages/__tests__/IntegrationsManager.test.tsx
@@ -202,10 +202,15 @@ describe('IntegrationsManager', () => {
expect(screen.getByText('Available providers')).toBeInTheDocument();
});
- it('renders page heading', () => {
+ it('omits the redundant page heading supplied by the app top bar', () => {
renderPage();
- expect(screen.getByRole('heading', { level: 2, name: /^Connections$/ })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('heading', { level: 2, name: /^Connections$/ }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.getByRole('heading', { level: 2, name: /Active connections/i }),
+ ).toBeInTheDocument();
});
it('renders section headings', () => {
diff --git a/frontend/src/pages/__tests__/McpLibraryPage.test.tsx b/frontend/src/pages/__tests__/McpLibraryPage.test.tsx
index 0fe8cd5a..274781a3 100644
--- a/frontend/src/pages/__tests__/McpLibraryPage.test.tsx
+++ b/frontend/src/pages/__tests__/McpLibraryPage.test.tsx
@@ -255,10 +255,13 @@ describe('McpLibraryPage', () => {
expect(screen.getByText('Add Server')).toBeInTheDocument();
});
- it('renders page heading', () => {
+ it('omits the redundant page heading supplied by the app top bar', () => {
renderPage();
- expect(screen.getByRole('heading', { level: 2, name: /MCP Library/i })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('heading', { level: 2, name: /^MCP Library$/ }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByPlaceholderText('Filter by server name')).toBeInTheDocument();
});
it('renders loading skeletons when isLoading is true and no servers', () => {
diff --git a/frontend/src/pages/__tests__/SchedulesPage.test.tsx b/frontend/src/pages/__tests__/SchedulesPage.test.tsx
index d9c21e62..43910cc9 100644
--- a/frontend/src/pages/__tests__/SchedulesPage.test.tsx
+++ b/frontend/src/pages/__tests__/SchedulesPage.test.tsx
@@ -166,11 +166,14 @@ describe('SchedulesPage', () => {
cleanup();
});
- it('renders page heading', () => {
+ it('omits the redundant page heading supplied by the app top bar', () => {
setupStore();
renderPage();
- expect(screen.getByRole('heading', { level: 2, name: /Schedules/i })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('heading', { level: 2, name: /^Schedules$/ }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /New schedule/i })).toBeInTheDocument();
});
it('renders loading skeletons when isLoading is true and no data', () => {
diff --git a/frontend/src/pages/__tests__/TemplateLibraryPage.test.tsx b/frontend/src/pages/__tests__/TemplateLibraryPage.test.tsx
index cfe0a4d4..05ba3d10 100644
--- a/frontend/src/pages/__tests__/TemplateLibraryPage.test.tsx
+++ b/frontend/src/pages/__tests__/TemplateLibraryPage.test.tsx
@@ -246,10 +246,13 @@ describe('TemplateLibraryPage', () => {
expect(screen.getByPlaceholderText('Filter by template name')).toBeInTheDocument();
});
- it('renders page heading', () => {
+ it('omits the redundant page heading supplied by the app top bar', () => {
renderPage();
- expect(screen.getByRole('heading', { level: 2, name: /Templates/i })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('heading', { level: 2, name: /^Templates$/ }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByPlaceholderText('Filter by template name')).toBeInTheDocument();
});
it('renders loading skeletons when isLoading is true', () => {
diff --git a/frontend/src/pages/__tests__/WebhooksPage.test.tsx b/frontend/src/pages/__tests__/WebhooksPage.test.tsx
index 4433223f..f3a5237a 100644
--- a/frontend/src/pages/__tests__/WebhooksPage.test.tsx
+++ b/frontend/src/pages/__tests__/WebhooksPage.test.tsx
@@ -197,11 +197,12 @@ describe('WebhooksPage', () => {
]),
);
- it('renders page heading', () => {
+ it('omits the redundant page heading supplied by the app top bar', () => {
setupStore();
renderPage();
- expect(screen.getByRole('heading', { level: 2, name: /Webhooks/i })).toBeInTheDocument();
+ expect(screen.queryByRole('heading', { level: 2, name: /^Webhooks$/ })).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /New webhook/i })).toBeInTheDocument();
});
it('renders loading skeletons when isLoading is true and no data', () => {
diff --git a/frontend/src/pages/api-keys-manager/ApiKeysTable.tsx b/frontend/src/pages/api-keys-manager/ApiKeysTable.tsx
index fc4100da..f6ace262 100644
--- a/frontend/src/pages/api-keys-manager/ApiKeysTable.tsx
+++ b/frontend/src/pages/api-keys-manager/ApiKeysTable.tsx
@@ -134,7 +134,6 @@ export function ApiKeysTable({
return (
<>
Stored secrets
From d716b78aa06485a5f026727b81d14c4bc88ab3ce Mon Sep 17 00:00:00 2001
From: zebbern <185730623+zebbern@users.noreply.github.com>
Date: Sun, 21 Jun 2026 00:11:18 +0200
Subject: [PATCH 4/7] fix(frontend): align no-title toolbar controls
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
---
.../src/components/shared/PageToolbar.tsx | 92 ++++++++++++-------
.../shared/__tests__/PageToolbar.test.tsx | 37 ++++++++
2 files changed, 96 insertions(+), 33 deletions(-)
create mode 100644 frontend/src/components/shared/__tests__/PageToolbar.test.tsx
diff --git a/frontend/src/components/shared/PageToolbar.tsx b/frontend/src/components/shared/PageToolbar.tsx
index 1364335e..ab6f406b 100644
--- a/frontend/src/components/shared/PageToolbar.tsx
+++ b/frontend/src/components/shared/PageToolbar.tsx
@@ -46,6 +46,39 @@ export function PageToolbar({
onSearchChange?.(e.target.value);
};
+ const searchControl = (className: string) => (
+
+ );
+
return (
{/* Title row — h1 + help icon + actions */}
@@ -76,39 +109,13 @@ export function PageToolbar({
)}
- {/* Search row — input + (actions when no title) + filters */}
- {hasSearch && (
-
-
+ {/* Search row — input + filters/actions */}
+ {hasSearch && hasTitle && (
+
+ {searchControl('flex-1')}
{((!hasTitle && actions) || filters) && (
{filters}
@@ -118,6 +125,25 @@ export function PageToolbar({
)}
+ {hasSearch && !hasTitle && (
+
+ {searchControl('min-w-[16rem] flex-1 sm:flex-none sm:w-72')}
+ {actions && (
+
+ {actions}
+
+ )}
+ {filters && (
+
+ {filters}
+
+ )}
+
+ )}
+
{/* Filters-only row — title present but no built-in search */}
{hasTitle && !hasSearch && filters &&
{filters}
}
diff --git a/frontend/src/components/shared/__tests__/PageToolbar.test.tsx b/frontend/src/components/shared/__tests__/PageToolbar.test.tsx
new file mode 100644
index 00000000..92f621c4
--- /dev/null
+++ b/frontend/src/components/shared/__tests__/PageToolbar.test.tsx
@@ -0,0 +1,37 @@
+import { describe, it, afterEach, expect, mock } from 'bun:test';
+import { cleanup, render, screen } from '@testing-library/react';
+import { PageToolbar } from '@/components/shared/PageToolbar';
+
+describe('PageToolbar', () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it('keeps no-title search and actions aligned before wrapping filters', () => {
+ render(
+
Kanban}
+ filters={
+ <>
+
+
+ >
+ }
+ />,
+ );
+
+ const searchGroup = screen.getByTestId('page-toolbar-search');
+ const actionsGroup = screen.getByTestId('page-toolbar-actions');
+ const filtersGroup = screen.getByTestId('page-toolbar-filters');
+ const controlsRow = screen.getByTestId('page-toolbar-controls');
+
+ expect(screen.getByRole('searchbox', { name: 'Search findings' })).toBeInTheDocument();
+ expect(searchGroup).toHaveClass('min-w-[16rem]');
+ expect(controlsRow).toHaveClass('flex-wrap');
+ expect(controlsRow).toHaveClass('items-start');
+ expect(Array.from(controlsRow.children)).toEqual([searchGroup, actionsGroup, filtersGroup]);
+ });
+});
From 7c66644954928f6ae0ab962e3142c3165d08e5dd Mon Sep 17 00:00:00 2001
From: zebbern <185730623+zebbern@users.noreply.github.com>
Date: Sun, 21 Jun 2026 00:40:22 +0200
Subject: [PATCH 5/7] docs: add bug bounty template design
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
---
...026-06-21-bugbounty-cve-template-design.md | 107 ++++++++++++++++++
1 file changed, 107 insertions(+)
create mode 100644 docs/superpowers/specs/2026-06-21-bugbounty-cve-template-design.md
diff --git a/docs/superpowers/specs/2026-06-21-bugbounty-cve-template-design.md b/docs/superpowers/specs/2026-06-21-bugbounty-cve-template-design.md
new file mode 100644
index 00000000..8fd42b7f
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-21-bugbounty-cve-template-design.md
@@ -0,0 +1,107 @@
+# Bug Bounty and CVE Research Workflow Templates Design
+
+## Goal
+
+Add four verified workflow templates aimed at bug bounty hunters and CVE researchers. The templates are useful starting points for legal, authorized research: asset discovery, fingerprinting, CVE mapping, and research brief generation.
+
+## Approved Template Set
+
+### Bug Bounty Recon Triage
+
+Purpose: turn an in-scope root domain or target list into a prioritized recon report.
+
+Workflow shape:
+
+1. Entry point collects scope, exclusions, and optional rate-limit notes.
+2. `sentris.subfinder.run` discovers subdomains.
+3. `sentris.dnsx.run` filters resolvable hosts.
+4. `sentris.httpx.scan` fingerprints live HTTP services.
+5. `sentris.katana.run` crawls live applications.
+6. `core.logic.script` ranks interesting assets by status code, technologies, exposed paths, and unusual titles.
+7. `core.artifact.writer` writes the recon triage report.
+
+Required secrets: none by default.
+
+Category and tags: `bug-bounty`; `bug-bounty`, `recon`, `subdomains`, `httpx`, `katana`, `triage`.
+
+### CVE Impact Research Brief
+
+Purpose: take a CVE ID or product/version pair and produce a structured research brief.
+
+Workflow shape:
+
+1. Entry point collects CVE ID, affected product/version, deployment notes, and research objective.
+2. `core.http.request` fetches public CVE metadata from the NVD CVE API.
+3. `core.http.request` fetches CISA Known Exploited Vulnerabilities catalog metadata.
+4. `core.logic.script` normalizes references, affected-version notes, CVSS-like severity, exploit-context flags, and detection ideas.
+5. `core.artifact.writer` writes a research brief with impact, affected surface, validation ideas, and next steps.
+
+Required secrets: optional `nvd_api_key` for higher public API rate limits.
+
+Category and tags: `cve-research`; `cve`, `research`, `kev`, `exploitability`, `impact-analysis`.
+
+### Exposed Service CVE Mapper
+
+Purpose: map externally exposed services to likely CVE research candidates.
+
+Workflow shape:
+
+1. Entry point collects hosts, CIDR ranges, or domains plus authorization notes.
+2. `sentris.naabu.scan` identifies exposed ports.
+3. `sentris.httpx.scan` fingerprints HTTP services and technologies.
+4. `core.logic.script` extracts service names, versions, banners, and product hints.
+5. `core.http.request` enriches likely product/version pairs against the NVD CVE API.
+6. `core.logic.script` prioritizes candidate CVEs by exposure, confidence, and severity signals.
+7. `core.artifact.writer` writes a CVE candidate map.
+
+Required secrets: optional `nvd_api_key`.
+
+Category and tags: `cve-research`; `cve`, `service-fingerprinting`, `naabu`, `httpx`, `exposure`.
+
+### Web Attack Surface Quick Win Hunt
+
+Purpose: quickly surface common bug bounty leads from known live web assets.
+
+Workflow shape:
+
+1. Entry point collects live hosts or URLs, out-of-scope paths, and scan intensity.
+2. `sentris.httpx.scan` refreshes live-target metadata.
+3. `sentris.katana.run` crawls paths and endpoints.
+4. `sentris.nuclei.scan` runs safe, common web exposure and misconfiguration checks.
+5. `sentris.testssl.run` reviews TLS issues when HTTPS assets are present.
+6. `core.logic.script` deduplicates and ranks leads into quick wins, needs-review, and noisy results.
+7. `core.artifact.writer` writes a bounty-ready lead report.
+
+Required secrets: none by default.
+
+Category and tags: `bug-bounty`; `bug-bounty`, `web`, `nuclei`, `katana`, `tls`, `quick-wins`.
+
+## Architecture
+
+The implementation will follow the existing seed-template model. Each template will be one JSON file in `backend/scripts/seed-templates` with `_metadata`, `manifest`, `graph`, and `requiredSecrets`. No frontend page changes are required because the template library already reads backend template data.
+
+Each graph will use existing registered component IDs only. Entry point nodes will include `runtimeInputs: []` in `data.config.params` so the new templates are stricter than the older seeds. Graphs will include explicit positions and simple control-flow edges to avoid invalid duplicate input handles.
+
+## Data Flow
+
+Templates are loaded by the local seed script and auto-seed service into the `templates` table. When a user selects a template, `TemplateService.useTemplate` parses the graph with `WorkflowGraphSchema`, applies any secret mappings, and creates a workflow with validation skipped so the user can configure runtime-specific values after import.
+
+## Error Handling
+
+The templates will avoid hardcoded secrets and real private endpoints. Public API steps will be represented as configurable `core.http.request` nodes with example parameters and optional secrets. If a public metadata API is unavailable during an actual run, the downstream scripts will still produce a partial report from the available upstream data once the user configures the workflow.
+
+## Verification
+
+Add a dedicated seed-template test that validates the new seed files:
+
+1. JSON contains `_metadata`, `manifest`, `graph`, and `requiredSecrets`.
+2. `manifest.nodeCount` and `manifest.edgeCount` match the graph.
+3. Every edge references existing node IDs.
+4. Every new graph parses with `WorkflowGraphSchema`.
+5. Every new template can be passed through `TemplateService.useTemplate` in a unit test and create a workflow.
+
+After implementation, run focused backend tests first, then seed the local database and verify the four cards appear in `/templates`. Complete verification requires using at least one of the new templates to create a workflow and confirming the workflow opens successfully.
+
+## Out of Scope
+
+This batch will not add new scanner components, exploit modules, aggressive attack automation, account integrations, or frontend screens. The templates are intended for authorized research workflows and report generation, not exploitation.
From 45245f29ddb20858eab0dd90528be02887748ea3 Mon Sep 17 00:00:00 2001
From: zebbern <185730623+zebbern@users.noreply.github.com>
Date: Sun, 21 Jun 2026 01:22:56 +0200
Subject: [PATCH 6/7] feat: add bug bounty cve workflow templates
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
---
.../bug-bounty-recon-triage.json | 238 ++++
.../cve-impact-research-brief.json | 232 ++++
.../exposed-service-cve-mapper.json | 240 ++++
.../web-attack-surface-quick-win-hunt.json | 293 +++++
.../__tests__/seed-templates.spec.ts | 63 ++
.../__tests__/templates.service.spec.ts | 61 +
.../2026-06-21-bugbounty-cve-templates.md | 1002 +++++++++++++++++
...026-06-21-bugbounty-cve-template-design.md | 4 +-
.../core/__tests__/http-request.test.ts | 19 +
worker/src/components/core/http-request.ts | 2 +-
10 files changed, 2151 insertions(+), 3 deletions(-)
create mode 100644 backend/scripts/seed-templates/bug-bounty-recon-triage.json
create mode 100644 backend/scripts/seed-templates/cve-impact-research-brief.json
create mode 100644 backend/scripts/seed-templates/exposed-service-cve-mapper.json
create mode 100644 backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json
create mode 100644 backend/src/templates/__tests__/seed-templates.spec.ts
create mode 100644 docs/superpowers/plans/2026-06-21-bugbounty-cve-templates.md
diff --git a/backend/scripts/seed-templates/bug-bounty-recon-triage.json b/backend/scripts/seed-templates/bug-bounty-recon-triage.json
new file mode 100644
index 00000000..3af7a354
--- /dev/null
+++ b/backend/scripts/seed-templates/bug-bounty-recon-triage.json
@@ -0,0 +1,238 @@
+{
+ "_metadata": {
+ "name": "Bug Bounty Recon Triage",
+ "description": "Authorized bug bounty recon workflow that discovers subdomains, resolves live hosts, probes HTTP services, crawls applications, and writes a prioritized recon report.",
+ "category": "bug-bounty",
+ "tags": ["bug-bounty", "recon", "subdomains", "httpx", "katana", "triage"],
+ "author": "sentris-team",
+ "version": "1.0.0"
+ },
+ "manifest": {
+ "name": "Bug Bounty Recon Triage",
+ "description": "Turn in-scope domains into a prioritized recon report with subdomain discovery, DNS resolution, live HTTP probing, crawling, and triage.",
+ "version": "1.0.0",
+ "author": "sentris-team",
+ "category": "bug-bounty",
+ "tags": ["bug-bounty", "recon", "subdomains", "httpx", "katana", "triage"],
+ "entryPoint": "trigger_1",
+ "nodeCount": 8,
+ "edgeCount": 8
+ },
+ "graph": {
+ "name": "Bug Bounty Recon Triage",
+ "description": "Authorized recon triage workflow for bug bounty targets.",
+ "nodes": [
+ {
+ "id": "trigger_1",
+ "type": "core.workflow.entrypoint",
+ "position": { "x": 100, "y": 260 },
+ "data": {
+ "label": "Authorized Scope Input",
+ "config": {
+ "params": {
+ "runtimeInputs": [
+ {
+ "id": "domains",
+ "label": "In-scope root domains",
+ "type": "array",
+ "required": true,
+ "description": "Root domains approved by the bug bounty program."
+ },
+ {
+ "id": "authorizationNotes",
+ "label": "Authorization notes",
+ "type": "text",
+ "required": false,
+ "description": "Program scope, exclusions, and rate-limit notes."
+ }
+ ]
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "subfinder_discovery",
+ "type": "sentris.subfinder.run",
+ "position": { "x": 420, "y": 260 },
+ "data": {
+ "label": "Discover Subdomains",
+ "config": {
+ "params": {
+ "threads": 10,
+ "timeout": 30,
+ "allSources": false,
+ "recursive": false
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "dnsx_resolve",
+ "type": "sentris.dnsx.run",
+ "position": { "x": 740, "y": 260 },
+ "data": {
+ "label": "Resolve Live Hosts",
+ "config": {
+ "params": {
+ "recordTypes": ["A"],
+ "outputMode": "json",
+ "includeResponses": true,
+ "threads": 100,
+ "retryCount": 2
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "httpx_probe",
+ "type": "sentris.httpx.scan",
+ "position": { "x": 1060, "y": 260 },
+ "data": {
+ "label": "Probe HTTP Services",
+ "config": {
+ "params": {
+ "threads": 50,
+ "followRedirects": true,
+ "tlsProbe": true,
+ "preferHttps": true
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "extract_live_urls",
+ "type": "core.logic.script",
+ "position": { "x": 1380, "y": 120 },
+ "data": {
+ "label": "Extract Live URLs",
+ "config": {
+ "params": {
+ "variables": [{ "name": "httpResponses", "type": "list-json" }],
+ "returns": [{ "name": "liveUrls", "type": "list-text" }],
+ "code": "export function script(input) {\n const responses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\n const urls = responses.map((item) => item?.url || item?.finalUrl || item?.input).filter((value) => typeof value === 'string' && value.length > 0);\n return { liveUrls: Array.from(new Set(urls)) };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "katana_crawl",
+ "type": "sentris.katana.run",
+ "position": { "x": 1700, "y": 120 },
+ "data": {
+ "label": "Crawl Live Applications",
+ "config": {
+ "params": {
+ "depth": 2,
+ "headless": false,
+ "timeout": 300,
+ "scope": "strict"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "rank_recon",
+ "type": "core.logic.script",
+ "position": { "x": 2020, "y": 260 },
+ "data": {
+ "label": "Rank Recon Leads",
+ "config": {
+ "params": {
+ "variables": [
+ { "name": "httpResponses", "type": "list-json" },
+ { "name": "endpoints", "type": "list-text" }
+ ],
+ "returns": [{ "name": "report", "type": "json" }],
+ "code": "export function script(input) {\n const responses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\n const endpoints = Array.isArray(input.endpoints) ? input.endpoints : [];\n const interesting = responses.filter((item) => [200, 401, 403, 500].includes(Number(item?.statusCode))).map((item) => ({ url: item.url, statusCode: item.statusCode, title: item.title, technologies: item.technologies || [] }));\n return { report: { summary: { liveHosts: responses.length, crawledEndpoints: endpoints.length, interestingAssets: interesting.length }, interestingAssets: interesting.slice(0, 100), endpoints: endpoints.slice(0, 200), nextSteps: ['Review 401/403 assets for auth bypass', 'Inspect unusual titles and technologies', 'Run targeted checks only inside authorized scope'] } };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "artifact_report",
+ "type": "core.artifact.writer",
+ "position": { "x": 2340, "y": 260 },
+ "data": {
+ "label": "Save Recon Triage Report",
+ "config": {
+ "params": {
+ "fileExtension": ".json",
+ "mimeType": "application/json",
+ "saveToRunArtifacts": true,
+ "publishToArtifactLibrary": false
+ },
+ "inputOverrides": {
+ "artifactName": "bug-bounty-recon-triage-{{date}}"
+ }
+ }
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "trigger_1-subfinder_discovery-domains",
+ "source": "trigger_1",
+ "target": "subfinder_discovery",
+ "sourceHandle": "domains",
+ "targetHandle": "domains"
+ },
+ {
+ "id": "subfinder_discovery-dnsx_resolve-subdomains",
+ "source": "subfinder_discovery",
+ "target": "dnsx_resolve",
+ "sourceHandle": "subdomains",
+ "targetHandle": "domains"
+ },
+ {
+ "id": "dnsx_resolve-httpx_probe-resolvedHosts",
+ "source": "dnsx_resolve",
+ "target": "httpx_probe",
+ "sourceHandle": "resolvedHosts",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "httpx_probe-extract_live_urls-responses",
+ "source": "httpx_probe",
+ "target": "extract_live_urls",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "extract_live_urls-katana_crawl-liveUrls",
+ "source": "extract_live_urls",
+ "target": "katana_crawl",
+ "sourceHandle": "liveUrls",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "httpx_probe-rank_recon-responses",
+ "source": "httpx_probe",
+ "target": "rank_recon",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "katana_crawl-rank_recon-endpoints",
+ "source": "katana_crawl",
+ "target": "rank_recon",
+ "sourceHandle": "endpoints",
+ "targetHandle": "endpoints"
+ },
+ {
+ "id": "rank_recon-artifact_report-report",
+ "source": "rank_recon",
+ "target": "artifact_report",
+ "sourceHandle": "report",
+ "targetHandle": "content"
+ }
+ ]
+ },
+ "requiredSecrets": []
+}
diff --git a/backend/scripts/seed-templates/cve-impact-research-brief.json b/backend/scripts/seed-templates/cve-impact-research-brief.json
new file mode 100644
index 00000000..3c1e21a3
--- /dev/null
+++ b/backend/scripts/seed-templates/cve-impact-research-brief.json
@@ -0,0 +1,232 @@
+{
+ "_metadata": {
+ "name": "CVE Impact Research Brief",
+ "description": "Research workflow that takes a CVE ID, gathers public NVD and CISA KEV context, and writes an impact brief with exploitability notes and validation ideas.",
+ "category": "cve-research",
+ "tags": ["cve", "research", "nvd", "kev", "exploitability", "impact-analysis"],
+ "author": "sentris-team",
+ "version": "1.0.0"
+ },
+ "manifest": {
+ "name": "CVE Impact Research Brief",
+ "description": "Build a structured CVE research brief from NVD and CISA KEV public metadata.",
+ "version": "1.0.0",
+ "author": "sentris-team",
+ "category": "cve-research",
+ "tags": ["cve", "research", "nvd", "kev", "exploitability", "impact-analysis"],
+ "entryPoint": "trigger_1",
+ "nodeCount": 6,
+ "edgeCount": 9
+ },
+ "graph": {
+ "name": "CVE Impact Research Brief",
+ "description": "CVE research workflow using public vulnerability metadata.",
+ "nodes": [
+ {
+ "id": "trigger_1",
+ "type": "core.workflow.entrypoint",
+ "position": { "x": 100, "y": 260 },
+ "data": {
+ "label": "CVE Research Input",
+ "config": {
+ "params": {
+ "runtimeInputs": [
+ {
+ "id": "cveId",
+ "label": "CVE ID",
+ "type": "text",
+ "required": true,
+ "description": "CVE identifier, for example CVE-2024-12345."
+ },
+ {
+ "id": "product",
+ "label": "Product",
+ "type": "text",
+ "required": false,
+ "description": "Optional product name or technology under review."
+ },
+ {
+ "id": "version",
+ "label": "Version",
+ "type": "text",
+ "required": false,
+ "description": "Optional deployed or suspected affected version."
+ },
+ {
+ "id": "deploymentNotes",
+ "label": "Deployment notes",
+ "type": "text",
+ "required": false,
+ "description": "Internal context for authorized impact analysis."
+ }
+ ]
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "build_nvd_url",
+ "type": "core.logic.script",
+ "position": { "x": 420, "y": 160 },
+ "data": {
+ "label": "Build NVD Query URL",
+ "config": {
+ "params": {
+ "variables": [{ "name": "cveId", "type": "string" }],
+ "returns": [{ "name": "nvdUrl", "type": "string" }],
+ "code": "export function script(input) {\n const cveId = String(input.cveId || '').trim().toUpperCase();\n return { nvdUrl: `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${encodeURIComponent(cveId)}` };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "fetch_nvd",
+ "type": "core.http.request",
+ "position": { "x": 740, "y": 160 },
+ "data": {
+ "label": "Fetch NVD CVE Metadata",
+ "config": {
+ "params": {
+ "method": "GET",
+ "contentType": "application/json",
+ "authType": "none",
+ "timeout": 30000,
+ "failOnError": false
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "fetch_kev",
+ "type": "core.http.request",
+ "position": { "x": 740, "y": 360 },
+ "data": {
+ "label": "Fetch CISA KEV Catalog",
+ "config": {
+ "params": {
+ "method": "GET",
+ "contentType": "application/json",
+ "authType": "none",
+ "timeout": 30000,
+ "failOnError": false
+ },
+ "inputOverrides": {
+ "url": "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
+ }
+ }
+ }
+ },
+ {
+ "id": "assemble_research_brief",
+ "type": "core.logic.script",
+ "position": { "x": 1060, "y": 260 },
+ "data": {
+ "label": "Assemble Research Brief",
+ "config": {
+ "params": {
+ "variables": [
+ { "name": "cveId", "type": "string" },
+ { "name": "nvdData", "type": "json" },
+ { "name": "kevData", "type": "json" },
+ { "name": "product", "type": "string" },
+ { "name": "version", "type": "string" },
+ { "name": "deploymentNotes", "type": "string" }
+ ],
+ "returns": [{ "name": "brief", "type": "json" }],
+ "code": "export function script(input) {\n const cveId = String(input.cveId || '').trim().toUpperCase();\n const nvdItems = Array.isArray(input.nvdData?.vulnerabilities) ? input.nvdData.vulnerabilities : [];\n const cve = nvdItems[0]?.cve || {};\n const descriptions = Array.isArray(cve.descriptions) ? cve.descriptions : [];\n const description = descriptions.find((item) => item.lang === 'en')?.value || descriptions[0]?.value || 'No NVD description available.';\n const references = Array.isArray(cve.references?.referenceData) ? cve.references.referenceData : Array.isArray(cve.references) ? cve.references : [];\n const kevItems = Array.isArray(input.kevData?.vulnerabilities) ? input.kevData.vulnerabilities : [];\n const kev = kevItems.find((item) => String(item.cveID || '').toUpperCase() === cveId) || null;\n const metrics = cve.metrics || {};\n return { brief: { cveId, product: input.product || null, version: input.version || null, deploymentNotes: input.deploymentNotes || null, description, nvdPublished: cve.published || null, nvdLastModified: cve.lastModified || null, metrics, knownExploited: Boolean(kev), kev, references: references.slice(0, 20), nextSteps: ['Confirm whether the affected product and version are present in authorized scope', 'Review vendor advisory and patch guidance', 'Design non-destructive validation checks', 'Create detection notes for exposed affected assets'] } };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "artifact_report",
+ "type": "core.artifact.writer",
+ "position": { "x": 1380, "y": 260 },
+ "data": {
+ "label": "Save CVE Research Brief",
+ "config": {
+ "params": {
+ "fileExtension": ".json",
+ "mimeType": "application/json",
+ "saveToRunArtifacts": true,
+ "publishToArtifactLibrary": false
+ },
+ "inputOverrides": {
+ "artifactName": "cve-impact-research-brief-{{date}}"
+ }
+ }
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "trigger_1-build_nvd_url-cveId",
+ "source": "trigger_1",
+ "target": "build_nvd_url",
+ "sourceHandle": "cveId",
+ "targetHandle": "cveId"
+ },
+ {
+ "id": "build_nvd_url-fetch_nvd-nvdUrl",
+ "source": "build_nvd_url",
+ "target": "fetch_nvd",
+ "sourceHandle": "nvdUrl",
+ "targetHandle": "url"
+ },
+ {
+ "id": "trigger_1-assemble_research_brief-cveId",
+ "source": "trigger_1",
+ "target": "assemble_research_brief",
+ "sourceHandle": "cveId",
+ "targetHandle": "cveId"
+ },
+ {
+ "id": "trigger_1-assemble_research_brief-product",
+ "source": "trigger_1",
+ "target": "assemble_research_brief",
+ "sourceHandle": "product",
+ "targetHandle": "product"
+ },
+ {
+ "id": "trigger_1-assemble_research_brief-version",
+ "source": "trigger_1",
+ "target": "assemble_research_brief",
+ "sourceHandle": "version",
+ "targetHandle": "version"
+ },
+ {
+ "id": "trigger_1-assemble_research_brief-deploymentNotes",
+ "source": "trigger_1",
+ "target": "assemble_research_brief",
+ "sourceHandle": "deploymentNotes",
+ "targetHandle": "deploymentNotes"
+ },
+ {
+ "id": "fetch_nvd-assemble_research_brief-data",
+ "source": "fetch_nvd",
+ "target": "assemble_research_brief",
+ "sourceHandle": "data",
+ "targetHandle": "nvdData"
+ },
+ {
+ "id": "fetch_kev-assemble_research_brief-data",
+ "source": "fetch_kev",
+ "target": "assemble_research_brief",
+ "sourceHandle": "data",
+ "targetHandle": "kevData"
+ },
+ {
+ "id": "assemble_research_brief-artifact_report-brief",
+ "source": "assemble_research_brief",
+ "target": "artifact_report",
+ "sourceHandle": "brief",
+ "targetHandle": "content"
+ }
+ ]
+ },
+ "requiredSecrets": []
+}
diff --git a/backend/scripts/seed-templates/exposed-service-cve-mapper.json b/backend/scripts/seed-templates/exposed-service-cve-mapper.json
new file mode 100644
index 00000000..1c88b11e
--- /dev/null
+++ b/backend/scripts/seed-templates/exposed-service-cve-mapper.json
@@ -0,0 +1,240 @@
+{
+ "_metadata": {
+ "name": "Exposed Service CVE Mapper",
+ "description": "Authorized research workflow that scans exposed services, fingerprints HTTP technologies, queries public CVE metadata, and writes a prioritized candidate CVE map.",
+ "category": "cve-research",
+ "tags": ["cve", "service-fingerprinting", "naabu", "httpx", "exposure", "nvd"],
+ "author": "sentris-team",
+ "version": "1.0.0"
+ },
+ "manifest": {
+ "name": "Exposed Service CVE Mapper",
+ "description": "Map exposed services and detected technologies to likely public CVE research candidates.",
+ "version": "1.0.0",
+ "author": "sentris-team",
+ "category": "cve-research",
+ "tags": ["cve", "service-fingerprinting", "naabu", "httpx", "exposure", "nvd"],
+ "entryPoint": "trigger_1",
+ "nodeCount": 8,
+ "edgeCount": 8
+ },
+ "graph": {
+ "name": "Exposed Service CVE Mapper",
+ "description": "External exposure fingerprinting workflow for authorized CVE research.",
+ "nodes": [
+ {
+ "id": "trigger_1",
+ "type": "core.workflow.entrypoint",
+ "position": { "x": 100, "y": 260 },
+ "data": {
+ "label": "Authorized Targets Input",
+ "config": {
+ "params": {
+ "runtimeInputs": [
+ {
+ "id": "targets",
+ "label": "Authorized targets",
+ "type": "array",
+ "required": true,
+ "description": "Hosts, IPs, CIDRs, or domains authorized for service discovery."
+ },
+ {
+ "id": "authorizationNotes",
+ "label": "Authorization notes",
+ "type": "text",
+ "required": false,
+ "description": "Scope notes and rate limits for the engagement."
+ }
+ ]
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "naabu_ports",
+ "type": "sentris.naabu.scan",
+ "position": { "x": 420, "y": 260 },
+ "data": {
+ "label": "Discover Exposed Ports",
+ "config": {
+ "params": {
+ "topPorts": 100,
+ "rate": 500,
+ "retries": 1,
+ "enablePing": false
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "extract_http_targets",
+ "type": "core.logic.script",
+ "position": { "x": 740, "y": 260 },
+ "data": {
+ "label": "Build HTTP Probe Targets",
+ "config": {
+ "params": {
+ "variables": [{ "name": "openPorts", "type": "list-json" }],
+ "returns": [{ "name": "httpTargets", "type": "list-text" }],
+ "code": "export function script(input) {\n const ports = Array.isArray(input.openPorts) ? input.openPorts : [];\n const urls = [];\n for (const item of ports) {\n const host = item?.host || item?.ip;\n const port = Number(item?.port);\n if (!host || !Number.isFinite(port)) continue;\n if ([443, 8443, 9443].includes(port)) urls.push(`https://${host}:${port}`);\n else if ([80, 8080, 8000, 8888, 3000, 5000, 7001, 9000].includes(port)) urls.push(`http://${host}:${port}`);\n else urls.push(`http://${host}:${port}`, `https://${host}:${port}`);\n }\n return { httpTargets: Array.from(new Set(urls)) };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "httpx_fingerprint",
+ "type": "sentris.httpx.scan",
+ "position": { "x": 1060, "y": 260 },
+ "data": {
+ "label": "Fingerprint HTTP Services",
+ "config": {
+ "params": {
+ "threads": 50,
+ "followRedirects": true,
+ "tlsProbe": true,
+ "preferHttps": true
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "build_cve_queries",
+ "type": "core.logic.script",
+ "position": { "x": 1380, "y": 160 },
+ "data": {
+ "label": "Build CVE Search Query",
+ "config": {
+ "params": {
+ "variables": [{ "name": "httpResponses", "type": "list-json" }],
+ "returns": [
+ { "name": "nvdUrl", "type": "string" },
+ { "name": "fingerprints", "type": "json" }
+ ],
+ "code": "export function script(input) {\n const responses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\n const tech = Array.from(new Set(responses.flatMap((item) => Array.isArray(item?.technologies) ? item.technologies : []).map(String).filter(Boolean)));\n const titles = Array.from(new Set(responses.map((item) => item?.title).filter((value) => typeof value === 'string' && value.length > 0)));\n const keyword = tech[0] || titles[0] || 'web server';\n const nvdUrl = `https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${encodeURIComponent(keyword)}`;\n return { nvdUrl, fingerprints: { keyword, technologies: tech, titles, services: responses.map((item) => ({ url: item.url, statusCode: item.statusCode, title: item.title, technologies: item.technologies || [] })) } };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "fetch_nvd_candidates",
+ "type": "core.http.request",
+ "position": { "x": 1700, "y": 160 },
+ "data": {
+ "label": "Fetch Candidate CVEs",
+ "config": {
+ "params": {
+ "method": "GET",
+ "contentType": "application/json",
+ "authType": "none",
+ "timeout": 30000,
+ "failOnError": false
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "rank_cve_candidates",
+ "type": "core.logic.script",
+ "position": { "x": 2020, "y": 260 },
+ "data": {
+ "label": "Rank Candidate CVEs",
+ "config": {
+ "params": {
+ "variables": [
+ { "name": "fingerprints", "type": "json" },
+ { "name": "nvdData", "type": "json" }
+ ],
+ "returns": [{ "name": "report", "type": "json" }],
+ "code": "export function script(input) {\n const vulnerabilities = Array.isArray(input.nvdData?.vulnerabilities) ? input.nvdData.vulnerabilities : [];\n const candidates = vulnerabilities.slice(0, 50).map((item) => ({ id: item?.cve?.id, published: item?.cve?.published, lastModified: item?.cve?.lastModified, descriptions: item?.cve?.descriptions, metrics: item?.cve?.metrics, references: item?.cve?.references }));\n return { report: { summary: { fingerprintKeyword: input.fingerprints?.keyword || null, technologiesObserved: input.fingerprints?.technologies || [], servicesObserved: Array.isArray(input.fingerprints?.services) ? input.fingerprints.services.length : 0, candidateCves: candidates.length }, services: input.fingerprints?.services || [], candidates, nextSteps: ['Verify exact product and version before reporting impact', 'Prioritize externally reachable services with confirmed technology matches', 'Use only non-destructive validation inside authorized scope'] } };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "artifact_report",
+ "type": "core.artifact.writer",
+ "position": { "x": 2340, "y": 260 },
+ "data": {
+ "label": "Save CVE Candidate Map",
+ "config": {
+ "params": {
+ "fileExtension": ".json",
+ "mimeType": "application/json",
+ "saveToRunArtifacts": true,
+ "publishToArtifactLibrary": false
+ },
+ "inputOverrides": {
+ "artifactName": "exposed-service-cve-map-{{date}}"
+ }
+ }
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "trigger_1-naabu_ports-targets",
+ "source": "trigger_1",
+ "target": "naabu_ports",
+ "sourceHandle": "targets",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "naabu_ports-extract_http_targets-findings",
+ "source": "naabu_ports",
+ "target": "extract_http_targets",
+ "sourceHandle": "findings",
+ "targetHandle": "openPorts"
+ },
+ {
+ "id": "extract_http_targets-httpx_fingerprint-httpTargets",
+ "source": "extract_http_targets",
+ "target": "httpx_fingerprint",
+ "sourceHandle": "httpTargets",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "httpx_fingerprint-build_cve_queries-responses",
+ "source": "httpx_fingerprint",
+ "target": "build_cve_queries",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "build_cve_queries-fetch_nvd_candidates-nvdUrl",
+ "source": "build_cve_queries",
+ "target": "fetch_nvd_candidates",
+ "sourceHandle": "nvdUrl",
+ "targetHandle": "url"
+ },
+ {
+ "id": "build_cve_queries-rank_cve_candidates-fingerprints",
+ "source": "build_cve_queries",
+ "target": "rank_cve_candidates",
+ "sourceHandle": "fingerprints",
+ "targetHandle": "fingerprints"
+ },
+ {
+ "id": "fetch_nvd_candidates-rank_cve_candidates-data",
+ "source": "fetch_nvd_candidates",
+ "target": "rank_cve_candidates",
+ "sourceHandle": "data",
+ "targetHandle": "nvdData"
+ },
+ {
+ "id": "rank_cve_candidates-artifact_report-report",
+ "source": "rank_cve_candidates",
+ "target": "artifact_report",
+ "sourceHandle": "report",
+ "targetHandle": "content"
+ }
+ ]
+ },
+ "requiredSecrets": []
+}
diff --git a/backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json b/backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json
new file mode 100644
index 00000000..adcd00f6
--- /dev/null
+++ b/backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json
@@ -0,0 +1,293 @@
+{
+ "_metadata": {
+ "name": "Web Attack Surface Quick Win Hunt",
+ "description": "Authorized bug bounty workflow that refreshes live web metadata, crawls endpoints, runs safe exposure checks, reviews TLS posture, and writes a prioritized quick-win lead report.",
+ "category": "bug-bounty",
+ "tags": ["bug-bounty", "web", "nuclei", "katana", "tls", "quick-wins"],
+ "author": "sentris-team",
+ "version": "1.0.0"
+ },
+ "manifest": {
+ "name": "Web Attack Surface Quick Win Hunt",
+ "description": "Find common, authorized bug bounty leads from known live web assets using httpx, Katana, Nuclei, and testssl.",
+ "version": "1.0.0",
+ "author": "sentris-team",
+ "category": "bug-bounty",
+ "tags": ["bug-bounty", "web", "nuclei", "katana", "tls", "quick-wins"],
+ "entryPoint": "trigger_1",
+ "nodeCount": 9,
+ "edgeCount": 11
+ },
+ "graph": {
+ "name": "Web Attack Surface Quick Win Hunt",
+ "description": "Quick web lead triage workflow for authorized bug bounty assets.",
+ "nodes": [
+ {
+ "id": "trigger_1",
+ "type": "core.workflow.entrypoint",
+ "position": { "x": 100, "y": 300 },
+ "data": {
+ "label": "Live Web Assets Input",
+ "config": {
+ "params": {
+ "runtimeInputs": [
+ {
+ "id": "liveUrls",
+ "label": "Live URLs",
+ "type": "array",
+ "required": true,
+ "description": "Known live URLs or hosts from authorized scope."
+ },
+ {
+ "id": "outOfScopePaths",
+ "label": "Out-of-scope paths",
+ "type": "array",
+ "required": false,
+ "description": "Paths that should not be tested."
+ },
+ {
+ "id": "scanIntensity",
+ "label": "Scan intensity",
+ "type": "text",
+ "required": false,
+ "description": "Operator note such as safe, normal, or thorough."
+ }
+ ]
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "httpx_refresh",
+ "type": "sentris.httpx.scan",
+ "position": { "x": 420, "y": 300 },
+ "data": {
+ "label": "Refresh Live Metadata",
+ "config": {
+ "params": {
+ "threads": 50,
+ "followRedirects": true,
+ "tlsProbe": true,
+ "preferHttps": true
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "extract_live_urls",
+ "type": "core.logic.script",
+ "position": { "x": 740, "y": 300 },
+ "data": {
+ "label": "Extract Live and TLS Targets",
+ "config": {
+ "params": {
+ "variables": [{ "name": "httpResponses", "type": "list-json" }],
+ "returns": [
+ { "name": "liveUrls", "type": "list-text" },
+ { "name": "tlsTargets", "type": "list-text" }
+ ],
+ "code": "export function script(input) {\n const responses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\n const liveUrls = responses.map((item) => item?.url || item?.finalUrl || item?.input).filter((value) => typeof value === 'string' && value.length > 0);\n const tlsTargets = liveUrls.filter((url) => url.startsWith('https://')).map((url) => { try { const parsed = new URL(url); return `${parsed.hostname}:${parsed.port || '443'}`; } catch { return null; } }).filter((value) => typeof value === 'string' && value.length > 0);\n return { liveUrls: Array.from(new Set(liveUrls)), tlsTargets: Array.from(new Set(tlsTargets)) };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "katana_crawl",
+ "type": "sentris.katana.run",
+ "position": { "x": 1060, "y": 120 },
+ "data": {
+ "label": "Crawl Endpoints",
+ "config": {
+ "params": {
+ "depth": 2,
+ "headless": false,
+ "timeout": 300,
+ "scope": "strict"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "nuclei_quick_checks",
+ "type": "sentris.nuclei.scan",
+ "position": { "x": 1060, "y": 300 },
+ "data": {
+ "label": "Run Safe Quick Checks",
+ "config": {
+ "params": {
+ "rateLimit": 75,
+ "concurrency": 15,
+ "timeout": 8,
+ "retries": 1,
+ "includeRaw": false,
+ "followRedirects": true,
+ "updateTemplates": false,
+ "disableHttpx": true,
+ "severityFilter": ["info", "low", "medium", "high", "critical"]
+ },
+ "inputOverrides": {
+ "templatePaths": ["http/exposures/", "http/misconfiguration/", "http/takeovers/"]
+ }
+ }
+ }
+ },
+ {
+ "id": "select_tls_target",
+ "type": "core.logic.script",
+ "position": { "x": 1060, "y": 500 },
+ "data": {
+ "label": "Select TLS Target",
+ "config": {
+ "params": {
+ "variables": [{ "name": "tlsTargets", "type": "list-text" }],
+ "returns": [{ "name": "tlsTarget", "type": "string" }],
+ "code": "export function script(input) {\n const targets = Array.isArray(input.tlsTargets) ? input.tlsTargets : [];\n return { tlsTarget: targets[0] || 'no-https-target-provided.invalid:443' };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "testssl_review",
+ "type": "sentris.testssl.run",
+ "position": { "x": 1380, "y": 500 },
+ "data": {
+ "label": "Review TLS Posture",
+ "config": {
+ "params": {
+ "protocols": true,
+ "ciphers": true,
+ "vulnerabilities": true,
+ "timeout": 600
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "rank_quick_wins",
+ "type": "core.logic.script",
+ "position": { "x": 1700, "y": 300 },
+ "data": {
+ "label": "Rank Quick-Win Leads",
+ "config": {
+ "params": {
+ "variables": [
+ { "name": "httpResponses", "type": "list-json" },
+ { "name": "endpoints", "type": "list-text" },
+ { "name": "nucleiFindings", "type": "list-json" },
+ { "name": "tlsFindings", "type": "list-json" }
+ ],
+ "returns": [{ "name": "report", "type": "json" }],
+ "code": "export function script(input) {\n const httpResponses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\n const endpoints = Array.isArray(input.endpoints) ? input.endpoints : [];\n const nucleiFindings = Array.isArray(input.nucleiFindings) ? input.nucleiFindings : [];\n const tlsFindings = Array.isArray(input.tlsFindings) ? input.tlsFindings : [];\n const quickWins = nucleiFindings.filter((item) => ['medium', 'high', 'critical'].includes(String(item?.severity || '').toLowerCase())).map((item) => ({ source: 'nuclei', templateId: item.templateId, name: item.name, severity: item.severity, matchedAt: item.matchedAt }));\n return { report: { summary: { liveAssets: httpResponses.length, crawledEndpoints: endpoints.length, nucleiFindings: nucleiFindings.length, tlsFindings: tlsFindings.length, quickWins: quickWins.length }, quickWins: quickWins.slice(0, 100), needsReview: nucleiFindings.slice(0, 100), tlsFindings: tlsFindings.slice(0, 50), interestingEndpoints: endpoints.filter((url) => /admin|debug|api|graphql|swagger|login|backup/i.test(url)).slice(0, 100), nextSteps: ['Validate findings manually before submitting reports', 'Confirm each lead is in scope', 'Avoid destructive checks and respect program rate limits'] } };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "artifact_report",
+ "type": "core.artifact.writer",
+ "position": { "x": 2020, "y": 300 },
+ "data": {
+ "label": "Save Quick-Win Report",
+ "config": {
+ "params": {
+ "fileExtension": ".json",
+ "mimeType": "application/json",
+ "saveToRunArtifacts": true,
+ "publishToArtifactLibrary": false
+ },
+ "inputOverrides": {
+ "artifactName": "web-attack-surface-quick-wins-{{date}}"
+ }
+ }
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "trigger_1-httpx_refresh-liveUrls",
+ "source": "trigger_1",
+ "target": "httpx_refresh",
+ "sourceHandle": "liveUrls",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "httpx_refresh-extract_live_urls-responses",
+ "source": "httpx_refresh",
+ "target": "extract_live_urls",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "extract_live_urls-katana_crawl-liveUrls",
+ "source": "extract_live_urls",
+ "target": "katana_crawl",
+ "sourceHandle": "liveUrls",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "extract_live_urls-nuclei_quick_checks-liveUrls",
+ "source": "extract_live_urls",
+ "target": "nuclei_quick_checks",
+ "sourceHandle": "liveUrls",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "extract_live_urls-select_tls_target-tlsTargets",
+ "source": "extract_live_urls",
+ "target": "select_tls_target",
+ "sourceHandle": "tlsTargets",
+ "targetHandle": "tlsTargets"
+ },
+ {
+ "id": "select_tls_target-testssl_review-tlsTarget",
+ "source": "select_tls_target",
+ "target": "testssl_review",
+ "sourceHandle": "tlsTarget",
+ "targetHandle": "target"
+ },
+ {
+ "id": "httpx_refresh-rank_quick_wins-responses",
+ "source": "httpx_refresh",
+ "target": "rank_quick_wins",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "katana_crawl-rank_quick_wins-endpoints",
+ "source": "katana_crawl",
+ "target": "rank_quick_wins",
+ "sourceHandle": "endpoints",
+ "targetHandle": "endpoints"
+ },
+ {
+ "id": "nuclei_quick_checks-rank_quick_wins-findings",
+ "source": "nuclei_quick_checks",
+ "target": "rank_quick_wins",
+ "sourceHandle": "findings",
+ "targetHandle": "nucleiFindings"
+ },
+ {
+ "id": "testssl_review-rank_quick_wins-findings",
+ "source": "testssl_review",
+ "target": "rank_quick_wins",
+ "sourceHandle": "findings",
+ "targetHandle": "tlsFindings"
+ },
+ {
+ "id": "rank_quick_wins-artifact_report-report",
+ "source": "rank_quick_wins",
+ "target": "artifact_report",
+ "sourceHandle": "report",
+ "targetHandle": "content"
+ }
+ ]
+ },
+ "requiredSecrets": []
+}
diff --git a/backend/src/templates/__tests__/seed-templates.spec.ts b/backend/src/templates/__tests__/seed-templates.spec.ts
new file mode 100644
index 00000000..258d03b6
--- /dev/null
+++ b/backend/src/templates/__tests__/seed-templates.spec.ts
@@ -0,0 +1,63 @@
+import { describe, expect, it } from 'bun:test';
+import { existsSync, readFileSync } from 'node:fs';
+import { join } from 'node:path';
+import { compileWorkflowGraph } from '../../dsl/compiler';
+import { WorkflowGraphSchema } from '../../workflows/dto/workflow-graph.dto';
+
+const seedTemplatesDir = join(import.meta.dir, '../../../scripts/seed-templates');
+
+const newTemplateFiles = [
+ 'bug-bounty-recon-triage.json',
+ 'cve-impact-research-brief.json',
+ 'exposed-service-cve-mapper.json',
+ 'web-attack-surface-quick-win-hunt.json',
+];
+
+describe('new seed templates', () => {
+ for (const fileName of newTemplateFiles) {
+ it(`${fileName} exists and contains a valid workflow graph`, () => {
+ const filePath = join(seedTemplatesDir, fileName);
+
+ expect(existsSync(filePath), `${fileName} should exist`).toBe(true);
+
+ const template = JSON.parse(readFileSync(filePath, 'utf8'));
+ const graph = template.graph;
+
+ expect(template._metadata.name).toBe(template.manifest.name);
+ expect(template._metadata.category).toBe(template.manifest.category);
+ expect(template._metadata.tags).toEqual(template.manifest.tags);
+ expect(template.manifest.entryPoint).toBe('trigger_1');
+ expect(template.manifest.nodeCount).toBe(graph.nodes.length);
+ expect(template.manifest.edgeCount).toBe(graph.edges.length);
+
+ for (const requiredSecret of template.requiredSecrets ?? []) {
+ expect(requiredSecret.type).toBe('string');
+ }
+
+ const nodeIds = new Set(graph.nodes.map((node: { id: string }) => node.id));
+
+ for (const edge of graph.edges) {
+ expect(nodeIds.has(edge.source), `${fileName} edge ${edge.id} has unknown source`).toBe(
+ true,
+ );
+ expect(nodeIds.has(edge.target), `${fileName} edge ${edge.id} has unknown target`).toBe(
+ true,
+ );
+ expect(edge.sourceHandle == null).toBe(edge.targetHandle == null);
+ }
+
+ const parsedGraph = WorkflowGraphSchema.parse(graph);
+ const compiled = compileWorkflowGraph(parsedGraph);
+
+ expect(compiled.entrypoint.ref).toBe('trigger_1');
+ expect(compiled.actions.length).toBe(graph.nodes.length);
+ expect(compiled.edges.length).toBe(graph.edges.length);
+
+ const entrypoint = parsedGraph.nodes.find((node) => node.id === 'trigger_1');
+ const runtimeInputs = entrypoint?.data.config.params.runtimeInputs;
+
+ expect(Array.isArray(runtimeInputs)).toBe(true);
+ expect((runtimeInputs as unknown[]).length).toBeGreaterThan(0);
+ });
+ }
+});
diff --git a/backend/src/templates/__tests__/templates.service.spec.ts b/backend/src/templates/__tests__/templates.service.spec.ts
index 174be2cd..2fdf8f8a 100644
--- a/backend/src/templates/__tests__/templates.service.spec.ts
+++ b/backend/src/templates/__tests__/templates.service.spec.ts
@@ -1,5 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'bun:test';
import { HttpException, HttpStatus, NotFoundException } from '@nestjs/common';
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
import type { ServiceWorkflowResponse } from '../../workflows/dto/workflow-graph.dto';
import { TemplateService } from '../templates.service';
@@ -50,6 +52,30 @@ function makeTemplate(overrides: Record = {}) {
};
}
+const seedTemplateDir = join(import.meta.dir, '../../../scripts/seed-templates');
+const bugBountyCveTemplateFiles = [
+ 'bug-bounty-recon-triage.json',
+ 'cve-impact-research-brief.json',
+ 'exposed-service-cve-mapper.json',
+ 'web-attack-surface-quick-win-hunt.json',
+] as const;
+
+function loadSeedTemplate(fileName: (typeof bugBountyCveTemplateFiles)[number]) {
+ return JSON.parse(readFileSync(join(seedTemplateDir, fileName), 'utf8')) as {
+ _metadata: {
+ name: string;
+ description?: string;
+ category: string;
+ tags: string[];
+ author: string;
+ version: string;
+ };
+ manifest: Record;
+ graph: Record;
+ requiredSecrets: { name: string; type: string; description: string }[];
+ };
+}
+
// ---------------------------------------------------------------------------
// Suite
// ---------------------------------------------------------------------------
@@ -168,6 +194,41 @@ describe('TemplateService', () => {
});
});
+ it('creates workflows from the bug bounty and CVE research seed templates', async () => {
+ for (const fileName of bugBountyCveTemplateFiles) {
+ const seedTemplate = loadSeedTemplate(fileName);
+ const tpl = makeTemplate({
+ id: `tpl-${fileName}`,
+ name: seedTemplate._metadata.name,
+ description: seedTemplate._metadata.description ?? '',
+ category: seedTemplate._metadata.category,
+ tags: seedTemplate._metadata.tags,
+ author: seedTemplate._metadata.author,
+ version: seedTemplate._metadata.version,
+ manifest: seedTemplate.manifest,
+ graph: seedTemplate.graph,
+ requiredSecrets: seedTemplate.requiredSecrets,
+ });
+
+ const mockWorkflow = { id: `wf-${fileName}` } as unknown as ServiceWorkflowResponse;
+ templatesRepository.findById.mockResolvedValueOnce(tpl);
+ workflowsService.create.mockResolvedValueOnce(mockWorkflow);
+ templatesRepository.incrementPopularity.mockResolvedValueOnce(undefined);
+
+ const result = await service.useTemplate(tpl.id, {
+ workflowName: `${seedTemplate._metadata.name} Copy`,
+ });
+
+ expect(result.templateName).toBe(seedTemplate._metadata.name);
+ expect(result.workflow.id).toBe(`wf-${fileName}`);
+ }
+
+ expect(workflowsService.create).toHaveBeenCalledTimes(bugBountyCveTemplateFiles.length);
+ expect(templatesRepository.incrementPopularity).toHaveBeenCalledTimes(
+ bugBountyCveTemplateFiles.length,
+ );
+ });
+
it('throws NotFoundException when template is not found', async () => {
templatesRepository.findById.mockResolvedValue(null);
diff --git a/docs/superpowers/plans/2026-06-21-bugbounty-cve-templates.md b/docs/superpowers/plans/2026-06-21-bugbounty-cve-templates.md
new file mode 100644
index 00000000..1b71c6f4
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-21-bugbounty-cve-templates.md
@@ -0,0 +1,1002 @@
+# Bug Bounty and CVE Research Workflow Templates Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add four compile-clean workflow templates for bug bounty recon and CVE research, then verify they can be seeded, shown in the template library, and used to create workflows.
+
+**Architecture:** Implement this as local seed-template JSON files under `backend/scripts/seed-templates`, following the existing template ingestion model. Add backend tests that validate the new seed files structurally, parse them with `WorkflowGraphSchema`, compile them with `compileWorkflowGraph`, and pass them through `TemplateService.useTemplate`.
+
+**Tech Stack:** Bun test runner, NestJS backend, TypeScript, Zod workflow schemas, existing Sentris worker component registry, JSON seed templates.
+
+---
+
+## File Structure
+
+- Create `backend/scripts/seed-templates/bug-bounty-recon-triage.json`: authorized recon workflow from scope domains to a ranked recon artifact.
+- Create `backend/scripts/seed-templates/cve-impact-research-brief.json`: CVE/product research workflow using NVD and CISA KEV public metadata.
+- Create `backend/scripts/seed-templates/exposed-service-cve-mapper.json`: exposure-to-CVE candidate workflow using Naabu, httpx, and NVD enrichment.
+- Create `backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json`: live-web-asset workflow using httpx, Katana, Nuclei, testssl, and ranking logic.
+- Create `backend/src/templates/__tests__/seed-templates.spec.ts`: focused seed-template verification for the four new files.
+- Modify `backend/src/templates/__tests__/templates.service.spec.ts`: add a use-template test covering the four new seed graphs.
+
+## Template Graph Rules
+
+Use these exact graph conventions in every new JSON file:
+
+- `core.workflow.entrypoint` nodes must set `data.config.params.runtimeInputs`.
+- Use data edges with both `sourceHandle` and `targetHandle` for actual data flow.
+- Use control edges with no handles only when the target does not need data from the source.
+- Use `core.logic.script` transform nodes for incompatible output shapes.
+- Use `core.artifact.writer` as the final report sink.
+- Do not add Slack or other notification nodes; these templates should run without secrets unless the user configures optional NVD rate-limit keys.
+
+## Shared Script Patterns
+
+Use script params with this shape whenever a script node appears:
+
+```json
+{
+ "variables": [{ "name": "httpResponses", "type": "list-json" }],
+ "returns": [{ "name": "report", "type": "json" }],
+ "code": "export function script(input) { return { report: input }; }"
+}
+```
+
+Use a compact but complete script body in JSON strings. The implementation may use `\\n` escapes inside JSON values; keep the exported function name `script`.
+
+## Task 1: Add Failing Seed-Template Validation Test
+
+**Files:**
+
+- Create: `backend/src/templates/__tests__/seed-templates.spec.ts`
+
+- [x] **Step 1: Write the failing test**
+
+Create `backend/src/templates/__tests__/seed-templates.spec.ts` with this content:
+
+```ts
+import { describe, expect, it } from 'bun:test';
+import { existsSync, readFileSync } from 'node:fs';
+import { join } from 'node:path';
+
+import { compileWorkflowGraph } from '../../dsl/compiler';
+import { WorkflowGraphSchema } from '../../workflows/dto/workflow-graph.dto';
+
+const seedDir = join(import.meta.dir, '../../../scripts/seed-templates');
+
+const newTemplateFiles = [
+ 'bug-bounty-recon-triage.json',
+ 'cve-impact-research-brief.json',
+ 'exposed-service-cve-mapper.json',
+ 'web-attack-surface-quick-win-hunt.json',
+] as const;
+
+interface SeedTemplate {
+ _metadata: {
+ name: string;
+ description?: string;
+ category: string;
+ tags: string[];
+ author: string;
+ version: string;
+ };
+ manifest: {
+ name: string;
+ description?: string;
+ category?: string;
+ tags?: string[];
+ entryPoint?: string;
+ nodeCount?: number;
+ edgeCount?: number;
+ };
+ graph: {
+ name: string;
+ nodes: Array<{
+ id: string;
+ type: string;
+ data?: { config?: { params?: Record } };
+ }>;
+ edges: Array<{
+ id: string;
+ source: string;
+ target: string;
+ sourceHandle?: string;
+ targetHandle?: string;
+ }>;
+ };
+ requiredSecrets: Array<{ name: string; type: string; description: string }>;
+}
+
+function readTemplate(fileName: string): SeedTemplate {
+ const path = join(seedDir, fileName);
+ expect(existsSync(path)).toBe(true);
+ return JSON.parse(readFileSync(path, 'utf8')) as SeedTemplate;
+}
+
+describe('bug bounty and CVE seed templates', () => {
+ it('have the approved files present', () => {
+ for (const fileName of newTemplateFiles) {
+ expect(existsSync(join(seedDir, fileName))).toBe(true);
+ }
+ });
+
+ it('match manifest counts and reference valid nodes', () => {
+ for (const fileName of newTemplateFiles) {
+ const template = readTemplate(fileName);
+ const nodeIds = new Set(template.graph.nodes.map((node) => node.id));
+
+ expect(template._metadata.name).toBe(template.manifest.name);
+ expect(template._metadata.category).toBe(template.manifest.category);
+ expect(template._metadata.tags).toEqual(template.manifest.tags);
+ expect(template.manifest.nodeCount).toBe(template.graph.nodes.length);
+ expect(template.manifest.edgeCount).toBe(template.graph.edges.length);
+ expect(template.manifest.entryPoint).toBe('trigger_1');
+ expect(template.requiredSecrets.every((secret) => secret.type === 'string')).toBe(true);
+
+ for (const edge of template.graph.edges) {
+ expect(nodeIds.has(edge.source)).toBe(true);
+ expect(nodeIds.has(edge.target)).toBe(true);
+ expect(Boolean(edge.sourceHandle)).toBe(Boolean(edge.targetHandle));
+ }
+ }
+ });
+
+ it('parse and compile with the workflow graph schema and component registry', () => {
+ for (const fileName of newTemplateFiles) {
+ const template = readTemplate(fileName);
+ const graph = WorkflowGraphSchema.parse(template.graph);
+ const compiled = compileWorkflowGraph(graph);
+
+ expect(compiled.entrypoint.ref).toBe('trigger_1');
+ expect(compiled.actions.length).toBe(template.graph.nodes.length);
+ expect(compiled.edges.length).toBe(template.graph.edges.length);
+ }
+ });
+
+ it('declare runtime inputs on every entry point', () => {
+ for (const fileName of newTemplateFiles) {
+ const template = readTemplate(fileName);
+ const entryPoint = template.graph.nodes.find((node) => node.id === 'trigger_1');
+ const runtimeInputs = entryPoint?.data?.config?.params?.runtimeInputs;
+
+ expect(Array.isArray(runtimeInputs)).toBe(true);
+ expect((runtimeInputs as unknown[]).length).toBeGreaterThan(0);
+ }
+ });
+});
+```
+
+- [ ] **Step 2: Run the new test and verify it fails because files are missing**
+
+Run:
+
+```powershell
+bun --cwd backend test src/templates/__tests__/seed-templates.spec.ts
+```
+
+Expected: FAIL, with missing-file assertions for the four new seed JSON files.
+
+## Task 2: Add the Four Seed JSON Templates
+
+**Files:**
+
+- Create: `backend/scripts/seed-templates/bug-bounty-recon-triage.json`
+- Create: `backend/scripts/seed-templates/cve-impact-research-brief.json`
+- Create: `backend/scripts/seed-templates/exposed-service-cve-mapper.json`
+- Create: `backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json`
+- Test: `backend/src/templates/__tests__/seed-templates.spec.ts`
+
+- [x] **Step 1: Create `bug-bounty-recon-triage.json`**
+
+Use this node and edge structure:
+
+```json
+{
+ "_metadata": {
+ "name": "Bug Bounty Recon Triage",
+ "description": "Authorized bug bounty recon workflow that discovers subdomains, resolves live hosts, probes HTTP services, crawls applications, and writes a prioritized recon report.",
+ "category": "bug-bounty",
+ "tags": ["bug-bounty", "recon", "subdomains", "httpx", "katana", "triage"],
+ "author": "sentris-team",
+ "version": "1.0.0"
+ },
+ "manifest": {
+ "name": "Bug Bounty Recon Triage",
+ "description": "Turn in-scope domains into a prioritized recon report with subdomain discovery, DNS resolution, live HTTP probing, crawling, and triage.",
+ "version": "1.0.0",
+ "author": "sentris-team",
+ "category": "bug-bounty",
+ "tags": ["bug-bounty", "recon", "subdomains", "httpx", "katana", "triage"],
+ "entryPoint": "trigger_1",
+ "nodeCount": 8,
+ "edgeCount": 8
+ },
+ "graph": {
+ "name": "Bug Bounty Recon Triage",
+ "description": "Authorized recon triage workflow for bug bounty targets.",
+ "nodes": [
+ {
+ "id": "trigger_1",
+ "type": "core.workflow.entrypoint",
+ "position": { "x": 100, "y": 260 },
+ "data": {
+ "label": "Authorized Scope Input",
+ "config": {
+ "params": {
+ "runtimeInputs": [
+ {
+ "id": "domains",
+ "label": "In-scope root domains",
+ "type": "array",
+ "required": true,
+ "description": "Root domains approved by the bug bounty program."
+ },
+ {
+ "id": "authorizationNotes",
+ "label": "Authorization notes",
+ "type": "text",
+ "required": false,
+ "description": "Program scope, exclusions, and rate-limit notes."
+ }
+ ]
+ },
+ "inputOverrides": {}
+ }
+ }
+ }
+ ],
+ "edges": []
+ },
+ "requiredSecrets": []
+}
+```
+
+Add the remaining seven nodes after `trigger_1`:
+
+```json
+[
+ {
+ "id": "subfinder_discovery",
+ "type": "sentris.subfinder.run",
+ "position": { "x": 420, "y": 260 },
+ "data": {
+ "label": "Discover Subdomains",
+ "config": {
+ "params": { "threads": 10, "timeout": 30, "allSources": false, "recursive": false },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "dnsx_resolve",
+ "type": "sentris.dnsx.run",
+ "position": { "x": 740, "y": 260 },
+ "data": {
+ "label": "Resolve Live Hosts",
+ "config": {
+ "params": {
+ "recordTypes": ["A"],
+ "outputMode": "json",
+ "includeResponses": true,
+ "threads": 100,
+ "retryCount": 2
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "httpx_probe",
+ "type": "sentris.httpx.scan",
+ "position": { "x": 1060, "y": 260 },
+ "data": {
+ "label": "Probe HTTP Services",
+ "config": {
+ "params": { "threads": 50, "followRedirects": true, "tlsProbe": true, "preferHttps": true },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "extract_live_urls",
+ "type": "core.logic.script",
+ "position": { "x": 1380, "y": 120 },
+ "data": {
+ "label": "Extract Live URLs",
+ "config": {
+ "params": {
+ "variables": [{ "name": "httpResponses", "type": "list-json" }],
+ "returns": [{ "name": "liveUrls", "type": "list-text" }],
+ "code": "export function script(input) {\\n const responses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\\n const urls = responses.map((item) => item?.url || item?.finalUrl || item?.input).filter((value) => typeof value === 'string' && value.length > 0);\\n return { liveUrls: Array.from(new Set(urls)) };\\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "katana_crawl",
+ "type": "sentris.katana.run",
+ "position": { "x": 1700, "y": 120 },
+ "data": {
+ "label": "Crawl Live Applications",
+ "config": {
+ "params": { "depth": 2, "headless": false, "timeout": 300, "scope": "strict" },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "rank_recon",
+ "type": "core.logic.script",
+ "position": { "x": 2020, "y": 260 },
+ "data": {
+ "label": "Rank Recon Leads",
+ "config": {
+ "params": {
+ "variables": [
+ { "name": "httpResponses", "type": "list-json" },
+ { "name": "endpoints", "type": "list-text" }
+ ],
+ "returns": [{ "name": "report", "type": "json" }],
+ "code": "export function script(input) {\\n const responses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\\n const endpoints = Array.isArray(input.endpoints) ? input.endpoints : [];\\n const interesting = responses.filter((item) => [200, 401, 403, 500].includes(Number(item?.statusCode))).map((item) => ({ url: item.url, statusCode: item.statusCode, title: item.title, technologies: item.technologies || [] }));\\n return { report: { summary: { liveHosts: responses.length, crawledEndpoints: endpoints.length, interestingAssets: interesting.length }, interestingAssets: interesting.slice(0, 100), endpoints: endpoints.slice(0, 200), nextSteps: ['Review 401/403 assets for auth bypass', 'Inspect unusual titles and technologies', 'Run targeted checks only inside authorized scope'] } };\\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "artifact_report",
+ "type": "core.artifact.writer",
+ "position": { "x": 2340, "y": 260 },
+ "data": {
+ "label": "Save Recon Triage Report",
+ "config": {
+ "params": {
+ "fileExtension": ".json",
+ "mimeType": "application/json",
+ "saveToRunArtifacts": true,
+ "publishToArtifactLibrary": false
+ },
+ "inputOverrides": { "artifactName": "bug-bounty-recon-triage-{{date}}" }
+ }
+ }
+ }
+]
+```
+
+Set `graph.edges` exactly to:
+
+```json
+[
+ {
+ "id": "trigger_1-subfinder_discovery-domains",
+ "source": "trigger_1",
+ "target": "subfinder_discovery",
+ "sourceHandle": "domains",
+ "targetHandle": "domains"
+ },
+ {
+ "id": "subfinder_discovery-dnsx_resolve-subdomains",
+ "source": "subfinder_discovery",
+ "target": "dnsx_resolve",
+ "sourceHandle": "subdomains",
+ "targetHandle": "domains"
+ },
+ {
+ "id": "dnsx_resolve-httpx_probe-resolvedHosts",
+ "source": "dnsx_resolve",
+ "target": "httpx_probe",
+ "sourceHandle": "resolvedHosts",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "httpx_probe-extract_live_urls-responses",
+ "source": "httpx_probe",
+ "target": "extract_live_urls",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "extract_live_urls-katana_crawl-liveUrls",
+ "source": "extract_live_urls",
+ "target": "katana_crawl",
+ "sourceHandle": "liveUrls",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "httpx_probe-rank_recon-responses",
+ "source": "httpx_probe",
+ "target": "rank_recon",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "katana_crawl-rank_recon-endpoints",
+ "source": "katana_crawl",
+ "target": "rank_recon",
+ "sourceHandle": "endpoints",
+ "targetHandle": "endpoints"
+ },
+ {
+ "id": "rank_recon-artifact_report-report",
+ "source": "rank_recon",
+ "target": "artifact_report",
+ "sourceHandle": "report",
+ "targetHandle": "content"
+ }
+]
+```
+
+- [x] **Step 2: Create `cve-impact-research-brief.json`**
+
+Create a graph with 6 nodes and 9 edges:
+
+- `trigger_1`: `core.workflow.entrypoint`, runtime inputs `cveId` text required, `product` text optional, `version` text optional, `deploymentNotes` text optional.
+- `build_nvd_url`: `core.logic.script`, input `cveId` string, output `nvdUrl` string. Script returns `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${encodeURIComponent(input.cveId)}`.
+- `fetch_nvd`: `core.http.request`, params `{ "method": "GET", "contentType": "application/json", "authType": "none", "timeout": 30000, "failOnError": false }`.
+- `fetch_kev`: `core.http.request`, inputOverrides `{ "url": "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" }`, same GET params as `fetch_nvd`.
+- `assemble_research_brief`: `core.logic.script`, variables `cveId` string, `nvdData` json, `kevData` json, `product` string, `version` string, `deploymentNotes` string; returns `brief` json. Script checks whether `kevData.vulnerabilities` includes the CVE ID and returns a summary object.
+- `artifact_report`: `core.artifact.writer`, params JSON artifact settings, inputOverride artifact name `cve-impact-research-brief-{{date}}`.
+
+Use these edges:
+
+```json
+[
+ {
+ "id": "trigger_1-build_nvd_url-cveId",
+ "source": "trigger_1",
+ "target": "build_nvd_url",
+ "sourceHandle": "cveId",
+ "targetHandle": "cveId"
+ },
+ {
+ "id": "build_nvd_url-fetch_nvd-nvdUrl",
+ "source": "build_nvd_url",
+ "target": "fetch_nvd",
+ "sourceHandle": "nvdUrl",
+ "targetHandle": "url"
+ },
+ {
+ "id": "trigger_1-assemble_research_brief-cveId",
+ "source": "trigger_1",
+ "target": "assemble_research_brief",
+ "sourceHandle": "cveId",
+ "targetHandle": "cveId"
+ },
+ {
+ "id": "trigger_1-assemble_research_brief-product",
+ "source": "trigger_1",
+ "target": "assemble_research_brief",
+ "sourceHandle": "product",
+ "targetHandle": "product"
+ },
+ {
+ "id": "trigger_1-assemble_research_brief-version",
+ "source": "trigger_1",
+ "target": "assemble_research_brief",
+ "sourceHandle": "version",
+ "targetHandle": "version"
+ },
+ {
+ "id": "trigger_1-assemble_research_brief-deploymentNotes",
+ "source": "trigger_1",
+ "target": "assemble_research_brief",
+ "sourceHandle": "deploymentNotes",
+ "targetHandle": "deploymentNotes"
+ },
+ {
+ "id": "fetch_nvd-assemble_research_brief-data",
+ "source": "fetch_nvd",
+ "target": "assemble_research_brief",
+ "sourceHandle": "data",
+ "targetHandle": "nvdData"
+ },
+ {
+ "id": "fetch_kev-assemble_research_brief-data",
+ "source": "fetch_kev",
+ "target": "assemble_research_brief",
+ "sourceHandle": "data",
+ "targetHandle": "kevData"
+ },
+ {
+ "id": "assemble_research_brief-artifact_report-brief",
+ "source": "assemble_research_brief",
+ "target": "artifact_report",
+ "sourceHandle": "brief",
+ "targetHandle": "content"
+ }
+]
+```
+
+Set `_metadata.category` and `manifest.category` to `cve-research`, tags to `["cve", "research", "nvd", "kev", "exploitability", "impact-analysis"]`, and `requiredSecrets` to `[]`.
+
+- [x] **Step 3: Create `exposed-service-cve-mapper.json`**
+
+Create a graph with 8 nodes and 8 edges:
+
+- `trigger_1`: runtime inputs `targets` array required, `authorizationNotes` text optional.
+- `naabu_ports`: `sentris.naabu.scan`, params `{ "topPorts": 100, "rate": 500, "retries": 1, "enablePing": false }`.
+- `extract_http_targets`: `core.logic.script`, variables `openPorts` list-json; returns `httpTargets` list-text. Script converts each open port into `http://host:port` and `https://host:port` candidates for common web ports.
+- `httpx_fingerprint`: `sentris.httpx.scan`, params `{ "threads": 50, "followRedirects": true, "tlsProbe": true, "preferHttps": true }`.
+- `build_cve_queries`: `core.logic.script`, variables `httpResponses` list-json; returns `nvdUrl` string and `fingerprints` json. Script extracts unique technology strings and builds an NVD keyword query URL.
+- `fetch_nvd_candidates`: `core.http.request`, GET params with `failOnError: false`.
+- `rank_cve_candidates`: `core.logic.script`, variables `fingerprints` json and `nvdData` json; returns `report` json.
+- `artifact_report`: `core.artifact.writer`, JSON artifact settings, inputOverride artifact name `exposed-service-cve-map-{{date}}`.
+
+Use these edges:
+
+```json
+[
+ {
+ "id": "trigger_1-naabu_ports-targets",
+ "source": "trigger_1",
+ "target": "naabu_ports",
+ "sourceHandle": "targets",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "naabu_ports-extract_http_targets-findings",
+ "source": "naabu_ports",
+ "target": "extract_http_targets",
+ "sourceHandle": "findings",
+ "targetHandle": "openPorts"
+ },
+ {
+ "id": "extract_http_targets-httpx_fingerprint-httpTargets",
+ "source": "extract_http_targets",
+ "target": "httpx_fingerprint",
+ "sourceHandle": "httpTargets",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "httpx_fingerprint-build_cve_queries-responses",
+ "source": "httpx_fingerprint",
+ "target": "build_cve_queries",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "build_cve_queries-fetch_nvd_candidates-nvdUrl",
+ "source": "build_cve_queries",
+ "target": "fetch_nvd_candidates",
+ "sourceHandle": "nvdUrl",
+ "targetHandle": "url"
+ },
+ {
+ "id": "build_cve_queries-rank_cve_candidates-fingerprints",
+ "source": "build_cve_queries",
+ "target": "rank_cve_candidates",
+ "sourceHandle": "fingerprints",
+ "targetHandle": "fingerprints"
+ },
+ {
+ "id": "fetch_nvd_candidates-rank_cve_candidates-data",
+ "source": "fetch_nvd_candidates",
+ "target": "rank_cve_candidates",
+ "sourceHandle": "data",
+ "targetHandle": "nvdData"
+ },
+ {
+ "id": "rank_cve_candidates-artifact_report-report",
+ "source": "rank_cve_candidates",
+ "target": "artifact_report",
+ "sourceHandle": "report",
+ "targetHandle": "content"
+ }
+]
+```
+
+Set `manifest.nodeCount` to `8` and `manifest.edgeCount` to `8`. Set tags to `["cve", "service-fingerprinting", "naabu", "httpx", "exposure", "nvd"]`. Set `requiredSecrets` to `[]`.
+
+- [x] **Step 4: Create `web-attack-surface-quick-win-hunt.json`**
+
+Create a graph with 9 nodes and 11 edges:
+
+- `trigger_1`: runtime inputs `liveUrls` array required, `outOfScopePaths` array optional, `scanIntensity` text optional.
+- `httpx_refresh`: `sentris.httpx.scan`, params `{ "threads": 50, "followRedirects": true, "tlsProbe": true, "preferHttps": true }`.
+- `extract_live_urls`: `core.logic.script`, variables `httpResponses` list-json; returns `liveUrls` list-text and `tlsTargets` list-text.
+- `katana_crawl`: `sentris.katana.run`, params `{ "depth": 2, "headless": false, "timeout": 300, "scope": "strict" }`.
+- `nuclei_quick_checks`: `sentris.nuclei.scan`, params `{ "rateLimit": 75, "concurrency": 15, "timeout": 8, "retries": 1, "includeRaw": false, "followRedirects": true, "updateTemplates": false, "disableHttpx": true, "severityFilter": ["info", "low", "medium", "high", "critical"] }`, inputOverrides `{ "templatePaths": ["http/exposures/", "http/misconfiguration/", "http/takeovers/"] }`.
+- `select_tls_target`: `core.logic.script`, variables `tlsTargets` list-text; returns `tlsTarget` string.
+- `testssl_review`: `sentris.testssl.run`, params `{ "protocols": true, "ciphers": true, "vulnerabilities": true, "timeout": 600 }`.
+- `rank_quick_wins`: `core.logic.script`, variables `httpResponses` list-json, `endpoints` list-text, `nucleiFindings` list-json, `tlsFindings` list-json; returns `report` json.
+- `artifact_report`: `core.artifact.writer`, JSON artifact settings, artifact name `web-attack-surface-quick-wins-{{date}}`.
+
+Use these edges:
+
+```json
+[
+ {
+ "id": "trigger_1-httpx_refresh-liveUrls",
+ "source": "trigger_1",
+ "target": "httpx_refresh",
+ "sourceHandle": "liveUrls",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "httpx_refresh-extract_live_urls-responses",
+ "source": "httpx_refresh",
+ "target": "extract_live_urls",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "extract_live_urls-katana_crawl-liveUrls",
+ "source": "extract_live_urls",
+ "target": "katana_crawl",
+ "sourceHandle": "liveUrls",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "extract_live_urls-nuclei_quick_checks-liveUrls",
+ "source": "extract_live_urls",
+ "target": "nuclei_quick_checks",
+ "sourceHandle": "liveUrls",
+ "targetHandle": "targets"
+ },
+ {
+ "id": "extract_live_urls-select_tls_target-tlsTargets",
+ "source": "extract_live_urls",
+ "target": "select_tls_target",
+ "sourceHandle": "tlsTargets",
+ "targetHandle": "tlsTargets"
+ },
+ {
+ "id": "select_tls_target-testssl_review-tlsTarget",
+ "source": "select_tls_target",
+ "target": "testssl_review",
+ "sourceHandle": "tlsTarget",
+ "targetHandle": "target"
+ },
+ {
+ "id": "httpx_refresh-rank_quick_wins-responses",
+ "source": "httpx_refresh",
+ "target": "rank_quick_wins",
+ "sourceHandle": "responses",
+ "targetHandle": "httpResponses"
+ },
+ {
+ "id": "katana_crawl-rank_quick_wins-endpoints",
+ "source": "katana_crawl",
+ "target": "rank_quick_wins",
+ "sourceHandle": "endpoints",
+ "targetHandle": "endpoints"
+ },
+ {
+ "id": "nuclei_quick_checks-rank_quick_wins-findings",
+ "source": "nuclei_quick_checks",
+ "target": "rank_quick_wins",
+ "sourceHandle": "findings",
+ "targetHandle": "nucleiFindings"
+ },
+ {
+ "id": "testssl_review-rank_quick_wins-findings",
+ "source": "testssl_review",
+ "target": "rank_quick_wins",
+ "sourceHandle": "findings",
+ "targetHandle": "tlsFindings"
+ },
+ {
+ "id": "rank_quick_wins-artifact_report-report",
+ "source": "rank_quick_wins",
+ "target": "artifact_report",
+ "sourceHandle": "report",
+ "targetHandle": "content"
+ }
+]
+```
+
+Set `manifest.nodeCount` to `9` and `manifest.edgeCount` to `11`. Set tags to `["bug-bounty", "web", "nuclei", "katana", "tls", "quick-wins"]`. Set `requiredSecrets` to `[]`.
+
+- [x] **Step 5: Run the seed-template validation test**
+
+Run:
+
+```powershell
+bun --cwd backend test src/templates/__tests__/seed-templates.spec.ts
+```
+
+Expected: PASS. If it fails, fix the JSON counts, node IDs, edge handles, component params, or script port declarations until it passes.
+
+## Task 3: Add Use-Template Service Coverage for New Seeds
+
+**Files:**
+
+- Modify: `backend/src/templates/__tests__/templates.service.spec.ts`
+- Test: `backend/src/templates/__tests__/templates.service.spec.ts`
+
+- [x] **Step 1: Add imports at the top of `templates.service.spec.ts`**
+
+Add these imports after the existing imports:
+
+```ts
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+```
+
+- [x] **Step 2: Add seed-loading helpers below `makeTemplate`**
+
+Add this code:
+
+```ts
+const seedTemplateDir = join(import.meta.dir, '../../../scripts/seed-templates');
+const bugBountyCveTemplateFiles = [
+ 'bug-bounty-recon-triage.json',
+ 'cve-impact-research-brief.json',
+ 'exposed-service-cve-mapper.json',
+ 'web-attack-surface-quick-win-hunt.json',
+] as const;
+
+function loadSeedTemplate(fileName: (typeof bugBountyCveTemplateFiles)[number]) {
+ return JSON.parse(readFileSync(join(seedTemplateDir, fileName), 'utf8')) as {
+ _metadata: {
+ name: string;
+ description?: string;
+ category: string;
+ tags: string[];
+ author: string;
+ version: string;
+ };
+ manifest: Record;
+ graph: Record;
+ requiredSecrets: { name: string; type: string; description: string }[];
+ };
+}
+```
+
+- [x] **Step 3: Add the service test inside `describe('useTemplate', () => { ... })`**
+
+Add this test before the not-found test:
+
+```ts
+it('creates workflows from the bug bounty and CVE research seed templates', async () => {
+ for (const fileName of bugBountyCveTemplateFiles) {
+ const seedTemplate = loadSeedTemplate(fileName);
+ const tpl = makeTemplate({
+ id: `tpl-${fileName}`,
+ name: seedTemplate._metadata.name,
+ description: seedTemplate._metadata.description ?? '',
+ category: seedTemplate._metadata.category,
+ tags: seedTemplate._metadata.tags,
+ author: seedTemplate._metadata.author,
+ version: seedTemplate._metadata.version,
+ manifest: seedTemplate.manifest,
+ graph: seedTemplate.graph,
+ requiredSecrets: seedTemplate.requiredSecrets,
+ });
+
+ const mockWorkflow = { id: `wf-${fileName}` } as unknown as ServiceWorkflowResponse;
+ templatesRepository.findById.mockResolvedValueOnce(tpl);
+ workflowsService.create.mockResolvedValueOnce(mockWorkflow);
+ templatesRepository.incrementPopularity.mockResolvedValueOnce(undefined);
+
+ const result = await service.useTemplate(tpl.id, {
+ workflowName: `${seedTemplate._metadata.name} Copy`,
+ });
+
+ expect(result.templateName).toBe(seedTemplate._metadata.name);
+ expect(result.workflow.id).toBe(`wf-${fileName}`);
+ }
+
+ expect(workflowsService.create).toHaveBeenCalledTimes(bugBountyCveTemplateFiles.length);
+ expect(templatesRepository.incrementPopularity).toHaveBeenCalledTimes(
+ bugBountyCveTemplateFiles.length,
+ );
+});
+```
+
+- [x] **Step 4: Run the service test**
+
+Run:
+
+```powershell
+bun --cwd backend test src/templates/__tests__/templates.service.spec.ts
+```
+
+Expected: PASS.
+
+## Task 4: Run Focused Backend Verification
+
+**Files:**
+
+- Test: `backend/src/templates/__tests__/seed-templates.spec.ts`
+- Test: `backend/src/templates/__tests__/templates.service.spec.ts`
+- Test: `worker/src/components/core/__tests__/http-request.test.ts`
+
+- [x] **Step 1: Run both template test files together**
+
+Run:
+
+```powershell
+bun --cwd backend test src/templates/__tests__/seed-templates.spec.ts src/templates/__tests__/templates.service.spec.ts
+```
+
+Expected: PASS.
+
+- [x] **Step 2: Run the worker HTTP port regression test**
+
+Run:
+
+```powershell
+bun --cwd worker test src/components/core/__tests__/http-request.test.ts
+```
+
+Expected: PASS.
+
+- [x] **Step 3: Run backend typecheck**
+
+Run:
+
+```powershell
+Push-Location backend; bun run typecheck; Pop-Location
+```
+
+Expected: PASS.
+
+- [x] **Step 4: Run worker typecheck**
+
+Run:
+
+```powershell
+Push-Location worker; bun run typecheck; Pop-Location
+```
+
+Expected: PASS.
+
+- [x] **Step 5: Run backend lint**
+
+Run:
+
+```powershell
+Push-Location backend; bun run lint; Pop-Location
+```
+
+Expected: PASS.
+
+- [x] **Step 6: Run worker lint**
+
+Run:
+
+```powershell
+Push-Location worker; bun run lint; Pop-Location
+```
+
+Expected: PASS. If lint flags JSON formatting through pre-commit only, run `bunx prettier --write backend/scripts/seed-templates/bug-bounty-recon-triage.json backend/scripts/seed-templates/cve-impact-research-brief.json backend/scripts/seed-templates/exposed-service-cve-mapper.json backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json docs/superpowers/plans/2026-06-21-bugbounty-cve-templates.md`.
+
+## Task 5: Seed and Verify in the Local App
+
+**Files:**
+
+- Read: `.sentris-instance`
+- Run: `backend/scripts/seed-templates.ts`
+- Verify: `/templates` and `/workflows/:id`
+
+- [x] **Step 1: Check the active instance**
+
+Run:
+
+```powershell
+just instance show
+```
+
+Expected: print the active instance. If `just` is unavailable in PowerShell, read `.sentris-instance` and use instance `0` only if the browser is on `http://localhost:5173`.
+
+Result: `just` was unavailable, `.sentris-instance` was absent, and the browser was on `http://localhost:5173/templates`, so instance `0` was used.
+
+- [x] **Step 2: Confirm backend health**
+
+For instance `0`, run:
+
+```powershell
+curl.exe -sf http://localhost:3211/api/v1/health
+```
+
+Expected: HTTP 200 response. For instance `N`, use port `3211 + N*100`.
+
+Result: `http://localhost:3211/api/v1/health` returned `{"status":"ok","service":"sentris-backend",...}`. The unprefixed `/health` route returned 404 in this running app.
+
+- [x] **Step 3: Seed the templates into the local database**
+
+For instance `0`, run:
+
+```powershell
+$env:DATABASE_URL='postgresql://sentris:sentris@localhost:5433/sentris_instance_0'
+bun --cwd backend scripts/seed-templates.ts
+```
+
+Expected: output showing the new template names inserted or skipped if already present.
+
+Result: 4 inserted, 36 skipped.
+
+- [x] **Step 4: Verify the API lists the four new templates**
+
+For instance `0`, run:
+
+```powershell
+$templates = Invoke-RestMethod -Uri http://localhost:3211/api/v1/templates
+$templates | Where-Object { $_.name -in @(
+ 'Bug Bounty Recon Triage',
+ 'CVE Impact Research Brief',
+ 'Exposed Service CVE Mapper',
+ 'Web Attack Surface Quick Win Hunt'
+) } | Select-Object name, category
+```
+
+Expected: four rows with categories `bug-bounty`, `cve-research`, `cve-research`, `bug-bounty`.
+
+Result: direct backend and nginx API responses listed all four templates with expected categories, node/edge counts, and `requiredSecrets` count `0`.
+
+- [x] **Step 5: Use the new templates through the API**
+
+Use `Invoke-RestMethod` with `x-internal-token: local-internal-token` and `x-organization-id: default-org` to call `POST /api/v1/templates/:id/use`, fetch `GET /api/v1/workflows/:id/runtime-inputs`, compile with `POST /api/v1/workflows/:id/commit`, then delete each verification workflow.
+
+Expected: all four templates create workflows, expose runtime inputs, compile, and clean up.
+
+Result: all four template-created workflows committed successfully. Runtime input counts were 2, 4, 2, and 3 respectively. No `Codex Verify*` workflows remained after cleanup.
+
+- [x] **Step 6: Verify in the browser**
+
+Open `http://localhost:5173/templates`, search for `bug bounty`, and confirm:
+
+- `Bug Bounty Recon Triage` appears.
+- `Web Attack Surface Quick Win Hunt` appears.
+- Searching `CVE` shows `CVE Impact Research Brief` and `Exposed Service CVE Mapper`.
+- Using at least one template creates a workflow and opens it under `/workflows/`.
+
+Result: after reloading `http://localhost:5173/templates`, the rendered page text included all four new template names.
+
+## Task 6: Commit Implementation
+
+**Files:**
+
+- Add all four JSON seed files.
+- Add `backend/src/templates/__tests__/seed-templates.spec.ts`.
+- Modify `backend/src/templates/__tests__/templates.service.spec.ts`.
+- Modify `worker/src/components/core/http-request.ts`.
+- Modify `worker/src/components/core/__tests__/http-request.test.ts`.
+- Include this plan file if it is still uncommitted.
+
+- [ ] **Step 1: Check git status**
+
+Run:
+
+```powershell
+git status --short
+```
+
+Expected: only the planned files are modified or added.
+
+- [ ] **Step 2: Stage planned files**
+
+Run:
+
+```powershell
+git add backend/scripts/seed-templates/bug-bounty-recon-triage.json `
+ backend/scripts/seed-templates/cve-impact-research-brief.json `
+ backend/scripts/seed-templates/exposed-service-cve-mapper.json `
+ backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json `
+ backend/src/templates/__tests__/seed-templates.spec.ts `
+ backend/src/templates/__tests__/templates.service.spec.ts `
+ worker/src/components/core/http-request.ts `
+ worker/src/components/core/__tests__/http-request.test.ts `
+ docs/superpowers/plans/2026-06-21-bugbounty-cve-templates.md
+```
+
+Expected: files staged.
+
+- [ ] **Step 3: Commit**
+
+Run:
+
+```powershell
+git commit -s -m "feat: add bug bounty cve workflow templates"
+```
+
+Expected: commit succeeds after pre-commit formatting.
+
+## Self-Review Notes
+
+- Spec coverage: the plan creates the four approved templates, tests schema/compile/use-template behavior, seeds locally, and verifies `/templates`.
+- Placeholder scan: no implementation step uses empty placeholder markers or unspecified code paths.
+- Type consistency: `sourceHandle` and `targetHandle` names match component ports inspected in `worker/src/components`.
+- Scope: no new components, no frontend pages, no exploitation automation, no notification integrations.
diff --git a/docs/superpowers/specs/2026-06-21-bugbounty-cve-template-design.md b/docs/superpowers/specs/2026-06-21-bugbounty-cve-template-design.md
index 8fd42b7f..463eb4a0 100644
--- a/docs/superpowers/specs/2026-06-21-bugbounty-cve-template-design.md
+++ b/docs/superpowers/specs/2026-06-21-bugbounty-cve-template-design.md
@@ -36,7 +36,7 @@ Workflow shape:
4. `core.logic.script` normalizes references, affected-version notes, CVSS-like severity, exploit-context flags, and detection ideas.
5. `core.artifact.writer` writes a research brief with impact, affected surface, validation ideas, and next steps.
-Required secrets: optional `nvd_api_key` for higher public API rate limits.
+Required secrets: none. NVD requests use the public unauthenticated API path so the template remains immediately usable from the template modal.
Category and tags: `cve-research`; `cve`, `research`, `kev`, `exploitability`, `impact-analysis`.
@@ -54,7 +54,7 @@ Workflow shape:
6. `core.logic.script` prioritizes candidate CVEs by exposure, confidence, and severity signals.
7. `core.artifact.writer` writes a CVE candidate map.
-Required secrets: optional `nvd_api_key`.
+Required secrets: none. NVD candidate lookup uses the public unauthenticated API path so the workflow can be created without secret mapping.
Category and tags: `cve-research`; `cve`, `service-fingerprinting`, `naabu`, `httpx`, `exposure`.
diff --git a/worker/src/components/core/__tests__/http-request.test.ts b/worker/src/components/core/__tests__/http-request.test.ts
index fb902aee..3595d2be 100644
--- a/worker/src/components/core/__tests__/http-request.test.ts
+++ b/worker/src/components/core/__tests__/http-request.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, test, beforeAll, afterAll } from 'bun:test';
import { definition } from '../http-request';
import type { ExecutionContext } from '@sentris/component-sdk';
+import { extractPorts } from '@sentris/component-sdk/zod-ports';
// Helper to create a dummy context
const mockContext: ExecutionContext = {
@@ -74,6 +75,24 @@ describe('HTTP Request Component', () => {
server.stop();
});
+ test('resolvePorts preserves response outputs when auth inputs are dynamic', () => {
+ const resolvedPorts = definition.resolvePorts?.({
+ method: 'GET',
+ contentType: 'application/json',
+ authType: 'bearer',
+ timeout: 1000,
+ failOnError: true,
+ });
+
+ expect(resolvedPorts?.outputs).toBeDefined();
+
+ const outputIds = extractPorts(resolvedPorts!.outputs!).map((port) => port.id);
+ expect(outputIds).toContain('status');
+ expect(outputIds).toContain('data');
+ expect(outputIds).toContain('headers');
+ expect(outputIds).toContain('rawBody');
+ });
+
test('should handle basic GET request', async () => {
const result = await definition.execute(
{
diff --git a/worker/src/components/core/http-request.ts b/worker/src/components/core/http-request.ts
index 00232de1..d68f4d04 100644
--- a/worker/src/components/core/http-request.ts
+++ b/worker/src/components/core/http-request.ts
@@ -211,7 +211,7 @@ const definition = defineComponent({
});
}
- return { inputs: inputs(inputShape) };
+ return { inputs: inputs(inputShape), outputs: outputSchema };
},
async execute({ inputs, params }, context) {
const { method, contentType, timeout, failOnError, authType } = params;
From 0354951cbaa66f2de36faab9cf0a487b61b07c75 Mon Sep 17 00:00:00 2001
From: zebbern <185730623+zebbern@users.noreply.github.com>
Date: Sun, 21 Jun 2026 06:28:07 +0200
Subject: [PATCH 7/7] changes
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
---
AGENTS.md | 83 +--
.../ai-vulnerability-triage.json | 139 ----
.../seed-templates/api-security-scan.json | 129 ----
.../bug-bounty-recon-triage.json | 43 +-
.../certificate-transparency-monitor.json | 116 ---
.../seed-templates/cloud-asset-inventory.json | 135 ----
.../cloud-compliance-audit.json | 104 ---
.../cloud-compliance-check.json | 92 ---
.../seed-templates/cloud-iam-audit.json | 132 ----
.../cloud-security-posture-audit.json | 118 ----
.../container-security-scan.json | 111 ---
.../cve-impact-research-brief.json | 160 +++--
.../seed-templates/dns-bruteforce-scan.json | 99 ---
.../seed-templates/dns-security-scan.json | 107 ---
.../seed-templates/email-header-analysis.json | 107 ---
.../employee-offboarding-security.json | 178 -----
.../exposed-service-cve-mapper.json | 145 +++-
.../github-repo-secret-scan.json | 91 ---
.../github-repo-vulnerability-scan.json | 108 ---
.../seed-templates/github-sast-scan.json | 87 ---
.../seed-templates/iac-security-scan.json | 121 ----
.../incident-response-triage.json | 100 ---
.../ioc-enrichment-workflow.json | 142 ----
.../kubernetes-security-audit.json | 159 -----
.../seed-templates/log-analysis-pipeline.json | 124 ----
.../malware-indicator-scan.json | 122 ----
.../network-recon-pipeline.json | 106 ---
.../npm-dependency-cve-hunt.json | 159 +++++
.../seed-templates/npm-dependency-scan.json | 83 ---
.../seed-templates/osint-reconnaissance.json | 99 ---
.../phishing-email-analysis.json | 92 ---
.../seed-templates/pip-dependency-scan.json | 83 ---
.../scripts/seed-templates/port-scanning.json | 94 ---
.../scripts/seed-templates/ssl-tls-audit.json | 85 ---
.../seed-templates/subdomain-enumeration.json | 111 ---
.../supabase-security-audit.json | 98 ---
.../vulnerability-scanning-pipeline.json | 82 ---
.../scripts/seed-templates/waf-detection.json | 95 ---
.../seed-templates/web-application-scan.json | 98 ---
.../web-attack-surface-quick-win-hunt.json | 19 +-
.../seed-templates/web-crawl-discovery.json | 96 ---
.../seed-templates/web-fuzzing-scan.json | 97 ---
.../__tests__/seed-templates.spec.ts | 227 ++++++
.../__tests__/templates.service.spec.ts | 13 +-
.../2026-06-21-osv-dependency-component.md | 169 +++++
...6-21-template-library-live-verification.md | 143 ++++
.../src/__tests__/har-builder.test.ts | 39 ++
.../__tests__/http-instrumentation.test.ts | 37 +
.../src/__tests__/runner-docker-pull.test.ts | 89 +++
.../component-sdk/src/http/har-builder.ts | 20 +-
packages/component-sdk/src/runner.ts | 271 +++++--
.../template-library-live-audit-utils.test.ts | 150 ++++
scripts/template-library-live-audit-utils.ts | 287 ++++++++
scripts/template-library-live-audit.ts | 661 ++++++++++++++++++
.../core/__tests__/http-request.test.ts | 29 +
worker/src/components/core/http-request.ts | 42 +-
worker/src/components/index.ts | 2 +
.../security/__tests__/httpx.test.ts | 68 +-
.../security/__tests__/katana.test.ts | 36 +
.../security/__tests__/nuclei.test.ts | 23 +-
.../components/security/__tests__/nvd.test.ts | 173 +++++
.../components/security/__tests__/osv.test.ts | 160 +++++
worker/src/components/security/httpx.ts | 72 +-
worker/src/components/security/katana.ts | 98 ++-
worker/src/components/security/nuclei.ts | 43 +-
worker/src/components/security/nvd.ts | 455 ++++++++++++
worker/src/components/security/osv.ts | 538 ++++++++++++++
.../__tests__/workflow-diagnostics.test.ts | 18 +
.../activities/__tests__/mcp.activity.test.ts | 10 +
.../src/temporal/activities/mcp.activity.ts | 13 +-
worker/src/temporal/workflow-diagnostics.ts | 2 +-
71 files changed, 4146 insertions(+), 4291 deletions(-)
delete mode 100644 backend/scripts/seed-templates/ai-vulnerability-triage.json
delete mode 100644 backend/scripts/seed-templates/api-security-scan.json
delete mode 100644 backend/scripts/seed-templates/certificate-transparency-monitor.json
delete mode 100644 backend/scripts/seed-templates/cloud-asset-inventory.json
delete mode 100644 backend/scripts/seed-templates/cloud-compliance-audit.json
delete mode 100644 backend/scripts/seed-templates/cloud-compliance-check.json
delete mode 100644 backend/scripts/seed-templates/cloud-iam-audit.json
delete mode 100644 backend/scripts/seed-templates/cloud-security-posture-audit.json
delete mode 100644 backend/scripts/seed-templates/container-security-scan.json
delete mode 100644 backend/scripts/seed-templates/dns-bruteforce-scan.json
delete mode 100644 backend/scripts/seed-templates/dns-security-scan.json
delete mode 100644 backend/scripts/seed-templates/email-header-analysis.json
delete mode 100644 backend/scripts/seed-templates/employee-offboarding-security.json
delete mode 100644 backend/scripts/seed-templates/github-repo-secret-scan.json
delete mode 100644 backend/scripts/seed-templates/github-repo-vulnerability-scan.json
delete mode 100644 backend/scripts/seed-templates/github-sast-scan.json
delete mode 100644 backend/scripts/seed-templates/iac-security-scan.json
delete mode 100644 backend/scripts/seed-templates/incident-response-triage.json
delete mode 100644 backend/scripts/seed-templates/ioc-enrichment-workflow.json
delete mode 100644 backend/scripts/seed-templates/kubernetes-security-audit.json
delete mode 100644 backend/scripts/seed-templates/log-analysis-pipeline.json
delete mode 100644 backend/scripts/seed-templates/malware-indicator-scan.json
delete mode 100644 backend/scripts/seed-templates/network-recon-pipeline.json
create mode 100644 backend/scripts/seed-templates/npm-dependency-cve-hunt.json
delete mode 100644 backend/scripts/seed-templates/npm-dependency-scan.json
delete mode 100644 backend/scripts/seed-templates/osint-reconnaissance.json
delete mode 100644 backend/scripts/seed-templates/phishing-email-analysis.json
delete mode 100644 backend/scripts/seed-templates/pip-dependency-scan.json
delete mode 100644 backend/scripts/seed-templates/port-scanning.json
delete mode 100644 backend/scripts/seed-templates/ssl-tls-audit.json
delete mode 100644 backend/scripts/seed-templates/subdomain-enumeration.json
delete mode 100644 backend/scripts/seed-templates/supabase-security-audit.json
delete mode 100644 backend/scripts/seed-templates/vulnerability-scanning-pipeline.json
delete mode 100644 backend/scripts/seed-templates/waf-detection.json
delete mode 100644 backend/scripts/seed-templates/web-application-scan.json
delete mode 100644 backend/scripts/seed-templates/web-crawl-discovery.json
delete mode 100644 backend/scripts/seed-templates/web-fuzzing-scan.json
create mode 100644 docs/superpowers/plans/2026-06-21-osv-dependency-component.md
create mode 100644 docs/superpowers/plans/2026-06-21-template-library-live-verification.md
create mode 100644 packages/component-sdk/src/__tests__/har-builder.test.ts
create mode 100644 packages/component-sdk/src/__tests__/runner-docker-pull.test.ts
create mode 100644 scripts/__tests__/template-library-live-audit-utils.test.ts
create mode 100644 scripts/template-library-live-audit-utils.ts
create mode 100644 scripts/template-library-live-audit.ts
create mode 100644 worker/src/components/security/__tests__/katana.test.ts
create mode 100644 worker/src/components/security/__tests__/nvd.test.ts
create mode 100644 worker/src/components/security/__tests__/osv.test.ts
create mode 100644 worker/src/components/security/nvd.ts
create mode 100644 worker/src/components/security/osv.ts
create mode 100644 worker/src/temporal/__tests__/workflow-diagnostics.test.ts
diff --git a/AGENTS.md b/AGENTS.md
index 32598383..e858d00b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,6 +2,14 @@
Security workflow orchestration platform. Visual builder + Temporal for reliability.
+To ensure security automation workflows work correctly, values such as these can be set when running the workflow locally to ensure realistic testing(not limited to these but good examples):
+
+- Website: http://scanme.nmap.org/
+- Website: https://preview.owasp-juice.shop/#/
+- Any github repo
+- Any npm package
+- Any public API endpoint
+
## Stack
- `frontend/` — React + Vite
@@ -250,78 +258,3 @@ The `/analytics` page provides triage performance metrics derived from `finding_
- **SLA policy management** (`GET/PUT /findings/sla-policies`): Configurable per-org severity→deadline mappings. Admin-only write access via `@Roles('ADMIN')`. Atomic replacement via transactional delete+insert.
- **Database**: `sla_policies` table with unique `(organization_id, severity)` constraint. Time-series indexes on `finding_triage(organization_id, created_at)` and `(organization_id, severity_override, created_at)` for aggregation query performance.
- **Frontend**: recharts charts (AreaChart, BarChart, PieChart), MTTR KPI cards, top assignees table, SLA policy settings form. WCAG 2.2 AA accessible — visually-hidden data tables, `role="img"` containers, `aria-busy` loading states, `prefers-reduced-motion` support.
-
----
-
-## Recent Changes
-
-### Vulnerability Lifecycle Management (VLM) UI
-
-- **Kanban board** for finding triage with drag-drop status transitions between columns.
-- **7-state lifecycle**: `new` → `triaged` → `in_progress` → `fixed` → `verified` (terminal). Any non-terminal state can move to `wont_fix` or `accepted_risk`; both can reopen to `triaged`.
-- **Bulk triage actions**: Select up to 100 findings and apply status and/or assignee changes in a single operation.
-- **Finding detail sheet**: Slide-over panel with triage controls (status, assignee picker, severity override, notes) and an activity timeline of all triage events.
-- **Hybrid data strategy**: Immutable finding data in OpenSearch, mutable triage state in PostgreSQL (`finding_triage` + `finding_triage_events` tables). Batch PG lookup merges triage records into OpenSearch results.
-- **Org member listing** via Clerk API integration for the assignee picker.
-- **State machine** (`packages/shared/src/finding-triage.ts`): Pure function shared between frontend and backend for transition validation.
-- **Status filter**: Filter the findings list by triage status.
-- **109 new tests** covering state machine, service, controller, kanban view, detail sheet, and bulk operations.
-
-### Webhook Inspector (Delivery Inspection & Resend)
-
-- **Response metadata capture**: Notification adapters now return response metadata (`durationMs`, `responseStatus`, `responseBody`) alongside success/error. The dispatcher stores these fields in the `notification_deliveries` table.
-- **Re-send capability**: Failed deliveries can be resent via the delivery detail panel. Resends create new delivery records through the same dispatch path (SSRF protection preserved). Rate limited to 10 resends per channel per minute.
-- **Delivery detail panel**: Collapsible accordion in delivery history shows full request/response inspection — request payload, response HTTP status, response body, and latency.
-- **Pagination**: Delivery history supports paginated loading via a "Load More" button.
-
-### Bidirectional Jira Ticketing
-
-- **OAuth 2.0 (3LO)** authentication with Atlassian Cloud. Encrypted token storage (AES-256-GCM) and automatic refresh with per-org mutex to prevent race conditions.
-- **Outbound sync**: When finding triage status changes (`finding.triage.changed` event via EventEmitter2), tickets are automatically created in Jira with matching status transitions.
-- **Inbound sync**: Jira webhooks notify status changes back. HMAC-SHA256 verified, reverse status mapping updates triage state. Circular sync prevention via `source` parameter.
-- **Frontend**: Settings > Ticketing tab for OAuth connection, project/issue type selection, status mapping configuration, and auto-create toggles. LinkedTicket component shows sync status in finding detail.
-- **Security**: SSRF allowlist (api.atlassian.com, auth.atlassian.com, \*.atlassian.net), issue key regex validation, org-scoped lookups, timing-safe HMAC verify.
-- **Database**: `ticketing_connections` (per-org provider config + encrypted tokens) and `ticket_links` (finding↔ticket mapping with sync status).
-- **78 tests** covering service, listener, controller, adapter, webhook handler, and frontend components.
-
-### Triage Trends & Analytics Dashboard
-
-- **Security posture trend**: Stacked area chart showing finding counts over time grouped by severity (critical/high/medium/low/info) with configurable time range (7d/30d/90d).
-- **Triage velocity**: Bar chart showing status transition throughput per time period — how many findings move through each triage state.
-- **MTTR KPI cards**: Mean Time to Remediation per severity with human-readable formatting (e.g., "2d 5h"). Null-safe for severities with no resolved findings.
-- **SLA compliance**: Per-severity compliance rate visualization. Color-coded bars (green ≥90%, yellow 50-89%, red <50%). Requires SLA policies to be configured.
-- **Status distribution**: Donut chart showing current breakdown across all 7 triage statuses with counts and percentages.
-- **Top assignees table**: Leaderboard ranking assignees by total findings, resolved count, and resolution rate. Includes unassigned findings.
-- **SLA policy configuration**: Admin-only settings to define per-org severity→deadline mappings (e.g., Critical=24h, High=72h). Stored in `sla_policies` table. Changes apply to future triage actions only.
-- **Accessibility**: WCAG 2.2 AA — visually-hidden data tables for screen readers, proper ARIA roles, contrast-safe colors, `prefers-reduced-motion` animation control.
-- **104 tests** covering backend aggregation queries, DTO validation, frontend chart components, and E2E endpoint verification.
-
----
-
-
-
-
-
-
-When tasks match a skill, load it: `cat .claude/skills//SKILL.md`
-
-
-
-
-component-development
-Creating components (inline/docker). Dynamic ports, retry policies, PTY patterns, IsolatedContainerVolume.
-project
-
-
-performance-review
-Review code changes for frontend performance anti-patterns. Checks stale times, bundle splitting, Zustand selectors, N+1 queries, and React rendering.
-project
-
-
-stress-test-frontend
-Run a frontend load testing audit. Seeds data, tests all pages via Chrome DevTools MCP, records network calls, TanStack queries, DOM sizes, and generates a timestamped report.
-project
-
-
-
-
diff --git a/backend/scripts/seed-templates/ai-vulnerability-triage.json b/backend/scripts/seed-templates/ai-vulnerability-triage.json
deleted file mode 100644
index 8ee8bdc8..00000000
--- a/backend/scripts/seed-templates/ai-vulnerability-triage.json
+++ /dev/null
@@ -1,139 +0,0 @@
-{
- "_metadata": {
- "name": "AI Vulnerability Triage",
- "description": "Automated vulnerability management pipeline that discovers live hosts with HTTPx, scans them with Nuclei, then uses an AI agent to prioritize findings by exploitability and business impact, generate an executive summary, and produce an actionable remediation report delivered via Slack.",
- "category": "vulnerability-management",
- "tags": [
- "ai",
- "vulnerability",
- "triage",
- "nuclei",
- "llm",
- "prioritization",
- "executive-summary"
- ],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "AI Vulnerability Triage",
- "description": "Scan for vulnerabilities, then use AI to prioritize findings and generate an executive summary.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "vulnerability-management",
- "tags": [
- "ai",
- "vulnerability",
- "triage",
- "nuclei",
- "llm",
- "prioritization",
- "executive-summary"
- ],
- "entryPoint": "trigger_1",
- "nodeCount": 7,
- "edgeCount": 6
- },
- "graph": {
- "name": "AI Vulnerability Triage",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": {
- "label": "Start Vulnerability Triage",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "httpx_discover",
- "type": "sentris.httpx.scan",
- "position": { "x": 400, "y": 300 },
- "data": {
- "label": "Discover Live Hosts",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "nuclei_scan",
- "type": "sentris.nuclei.scan",
- "position": { "x": 700, "y": 300 },
- "data": {
- "label": "Nuclei Vulnerability Scan",
- "config": {
- "params": {
- "severity": ["critical", "high", "medium"]
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "ai_triage",
- "type": "core.ai.generate-text",
- "position": { "x": 1000, "y": 300 },
- "data": {
- "label": "AI Prioritize & Summarize",
- "config": {
- "params": {
- "systemPrompt": "You are a senior vulnerability analyst. Analyze the scan results, prioritize findings by exploitability and business impact, and produce a structured executive summary with: 1) Critical findings requiring immediate action, 2) High-priority remediation recommendations, 3) Risk score summary, 4) Timeline recommendations."
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_format",
- "type": "core.logic.script",
- "position": { "x": 1300, "y": 300 },
- "data": {
- "label": "Format Triage Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1600, "y": 300 },
- "data": {
- "label": "Save Executive Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_team",
- "type": "core.notification.slack",
- "position": { "x": 1900, "y": 300 },
- "data": {
- "label": "Notify Security Leadership",
- "config": { "params": { "channel": "#vuln-management" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-httpx_discover", "source": "trigger_1", "target": "httpx_discover" },
- { "id": "httpx_discover-nuclei_scan", "source": "httpx_discover", "target": "nuclei_scan" },
- { "id": "nuclei_scan-ai_triage", "source": "nuclei_scan", "target": "ai_triage" },
- { "id": "ai_triage-script_format", "source": "ai_triage", "target": "script_format" },
- {
- "id": "script_format-artifact_report",
- "source": "script_format",
- "target": "artifact_report"
- },
- { "id": "artifact_report-notify_team", "source": "artifact_report", "target": "notify_team" }
- ]
- },
- "requiredSecrets": [
- {
- "name": "openai_api_key",
- "type": "string",
- "description": "OpenAI API key for AI-powered vulnerability triage and summarization"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for vulnerability triage notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/api-security-scan.json b/backend/scripts/seed-templates/api-security-scan.json
deleted file mode 100644
index 89b6c207..00000000
--- a/backend/scripts/seed-templates/api-security-scan.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
- "_metadata": {
- "name": "API Security Scan",
- "description": "Automated API security assessment that crawls web endpoints with Katana to discover API routes, probes them with HTTPx for live validation, then runs Nuclei with API-focused templates to test for authentication bypass, injection vulnerabilities, and misconfigurations. Results are evaluated, compiled into an artifact report, and delivered via Slack.",
- "category": "api-security",
- "tags": ["api", "rest", "injection", "auth-bypass", "nuclei", "katana"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "API Security Scan",
- "description": "Discover API endpoints, test for authentication bypass and injection vulnerabilities, and generate a security report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "api-security",
- "tags": ["api", "rest", "injection", "auth-bypass", "nuclei", "katana"],
- "entryPoint": "trigger_1",
- "nodeCount": 7,
- "edgeCount": 6
- },
- "graph": {
- "name": "API Security Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start API Security Scan",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "katana_crawl",
- "type": "sentris.katana.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Crawl & Discover API Endpoints",
- "config": {
- "params": {
- "depth": 3,
- "jsRendering": true,
- "scope": "same-domain"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "httpx_probe",
- "type": "sentris.httpx.scan",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Probe Live API Endpoints",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "nuclei_api",
- "type": "sentris.nuclei.scan",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Test API Vulnerabilities",
- "config": {
- "params": {
- "tags": ["api", "auth-bypass", "injection", "misconfig"],
- "severity": ["critical", "high", "medium"]
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_evaluate",
- "type": "core.logic.script",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Evaluate & Prioritize Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Generate API Security Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1900, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#api-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-katana_crawl", "source": "trigger_1", "target": "katana_crawl" },
- { "id": "katana_crawl-httpx_probe", "source": "katana_crawl", "target": "httpx_probe" },
- { "id": "httpx_probe-nuclei_api", "source": "httpx_probe", "target": "nuclei_api" },
- {
- "id": "nuclei_api-script_evaluate",
- "source": "nuclei_api",
- "target": "script_evaluate"
- },
- {
- "id": "script_evaluate-artifact_report",
- "source": "script_evaluate",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for API security scan notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/bug-bounty-recon-triage.json b/backend/scripts/seed-templates/bug-bounty-recon-triage.json
index 3af7a354..c10ecfbe 100644
--- a/backend/scripts/seed-templates/bug-bounty-recon-triage.json
+++ b/backend/scripts/seed-templates/bug-bounty-recon-triage.json
@@ -15,8 +15,8 @@
"category": "bug-bounty",
"tags": ["bug-bounty", "recon", "subdomains", "httpx", "katana", "triage"],
"entryPoint": "trigger_1",
- "nodeCount": 8,
- "edgeCount": 8
+ "nodeCount": 9,
+ "edgeCount": 10
},
"graph": {
"name": "Bug Bounty Recon Triage",
@@ -103,6 +103,25 @@
}
}
},
+ {
+ "id": "merge_probe_targets",
+ "type": "core.logic.script",
+ "position": { "x": 900, "y": 460 },
+ "data": {
+ "label": "Merge Probe Targets",
+ "config": {
+ "params": {
+ "variables": [
+ { "name": "domains", "type": "list-text" },
+ { "name": "resolvedHosts", "type": "list-text" }
+ ],
+ "returns": [{ "name": "targets", "type": "list-text" }],
+ "code": "export function script(input) {\n const values = [];\n const add = (items) => {\n if (!Array.isArray(items)) return;\n for (const item of items) {\n if (typeof item !== 'string') continue;\n const trimmed = item.trim();\n if (trimmed.length > 0) values.push(trimmed);\n }\n };\n add(input.domains);\n add(input.resolvedHosts);\n return { targets: Array.from(new Set(values)) };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
{
"id": "extract_live_urls",
"type": "core.logic.script",
@@ -129,7 +148,7 @@
"params": {
"depth": 2,
"headless": false,
- "timeout": 300,
+ "timeout": 30,
"scope": "strict"
},
"inputOverrides": {}
@@ -191,10 +210,24 @@
"targetHandle": "domains"
},
{
- "id": "dnsx_resolve-httpx_probe-resolvedHosts",
+ "id": "trigger_1-merge_probe_targets-domains",
+ "source": "trigger_1",
+ "target": "merge_probe_targets",
+ "sourceHandle": "domains",
+ "targetHandle": "domains"
+ },
+ {
+ "id": "dnsx_resolve-merge_probe_targets-resolvedHosts",
"source": "dnsx_resolve",
- "target": "httpx_probe",
+ "target": "merge_probe_targets",
"sourceHandle": "resolvedHosts",
+ "targetHandle": "resolvedHosts"
+ },
+ {
+ "id": "merge_probe_targets-httpx_probe-targets",
+ "source": "merge_probe_targets",
+ "target": "httpx_probe",
+ "sourceHandle": "targets",
"targetHandle": "targets"
},
{
diff --git a/backend/scripts/seed-templates/certificate-transparency-monitor.json b/backend/scripts/seed-templates/certificate-transparency-monitor.json
deleted file mode 100644
index 42e3f0af..00000000
--- a/backend/scripts/seed-templates/certificate-transparency-monitor.json
+++ /dev/null
@@ -1,116 +0,0 @@
-{
- "_metadata": {
- "name": "Certificate Transparency Monitor",
- "description": "Certificate Transparency log monitoring workflow that queries CT APIs (crt.sh) for certificates issued to target domains, filters results by issuance date, matches against a domain watchlist, and alerts on newly issued or unexpected certificates. Useful for detecting unauthorized certificate issuance, subdomain takeover attempts, and shadow IT discovery.",
- "category": "certificate-monitoring",
- "tags": ["certificate", "ct-logs", "transparency", "monitoring", "tls", "pki"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Certificate Transparency Monitor",
- "description": "Monitor CT logs for new certificates issued for target domains, detect anomalies, and alert on unexpected issuance.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "certificate-monitoring",
- "tags": ["certificate", "ct-logs", "transparency", "monitoring", "tls", "pki"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "Certificate Transparency Monitor",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start CT Log Monitor",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "http_ct_query",
- "type": "core.http.request",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Query crt.sh CT Logs",
- "config": {
- "params": {
- "method": "GET",
- "url": "https://crt.sh/?output=json"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_filter",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Filter & Match Domains",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_analyze",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Detect Anomalous Certificates",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Generate CT Monitoring Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Alert on New Certificates",
- "config": { "params": { "channel": "#cert-monitoring" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-http_ct_query", "source": "trigger_1", "target": "http_ct_query" },
- {
- "id": "http_ct_query-script_filter",
- "source": "http_ct_query",
- "target": "script_filter"
- },
- {
- "id": "script_filter-script_analyze",
- "source": "script_filter",
- "target": "script_analyze"
- },
- {
- "id": "script_analyze-artifact_report",
- "source": "script_analyze",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for certificate transparency alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/cloud-asset-inventory.json b/backend/scripts/seed-templates/cloud-asset-inventory.json
deleted file mode 100644
index ea011135..00000000
--- a/backend/scripts/seed-templates/cloud-asset-inventory.json
+++ /dev/null
@@ -1,135 +0,0 @@
-{
- "_metadata": {
- "name": "Cloud Asset Inventory",
- "description": "Cloud asset discovery and inventory workflow that authenticates to AWS, runs Prowler to enumerate cloud resources, classifies discovered assets by type and risk tier, audits resource tagging against organizational policy, and produces a comprehensive inventory report. Use for periodic cloud hygiene reviews and compliance audits.",
- "category": "cloud-security",
- "tags": ["cloud", "aws", "inventory", "asset-management", "compliance", "prowler"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Cloud Asset Inventory",
- "description": "Discover cloud resources, classify assets, audit tagging compliance, and generate an inventory report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "cloud-security",
- "tags": ["cloud", "aws", "inventory", "asset-management", "compliance", "prowler"],
- "entryPoint": "trigger_1",
- "nodeCount": 7,
- "edgeCount": 7
- },
- "graph": {
- "name": "Cloud Asset Inventory",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start Inventory Scan",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "aws_creds",
- "type": "core.credentials.aws",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Load AWS Credentials",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "prowler_discover",
- "type": "security.prowler.scan",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Discover Cloud Resources",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_classify",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 150 },
- "data": {
- "label": "Classify Assets by Risk",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_tag_audit",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 350 },
- "data": {
- "label": "Audit Resource Tags",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_inventory",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Generate Inventory Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_team",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Send Inventory Summary",
- "config": { "params": { "channel": "#cloud-ops" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-aws_creds", "source": "trigger_1", "target": "aws_creds" },
- { "id": "aws_creds-prowler_discover", "source": "aws_creds", "target": "prowler_discover" },
- {
- "id": "prowler_discover-script_classify",
- "source": "prowler_discover",
- "target": "script_classify"
- },
- {
- "id": "prowler_discover-script_tag_audit",
- "source": "prowler_discover",
- "target": "script_tag_audit"
- },
- {
- "id": "script_classify-artifact_inventory",
- "source": "script_classify",
- "target": "artifact_inventory"
- },
- {
- "id": "script_tag_audit-artifact_inventory",
- "source": "script_tag_audit",
- "target": "artifact_inventory"
- },
- {
- "id": "artifact_inventory-notify_team",
- "source": "artifact_inventory",
- "target": "notify_team"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "aws_access_key_id",
- "type": "string",
- "description": "AWS access key ID for the target account"
- },
- {
- "name": "aws_secret_access_key",
- "type": "string",
- "description": "AWS secret access key for the target account"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for cloud operations channel"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/cloud-compliance-audit.json b/backend/scripts/seed-templates/cloud-compliance-audit.json
deleted file mode 100644
index 1778cf92..00000000
--- a/backend/scripts/seed-templates/cloud-compliance-audit.json
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- "_metadata": {
- "name": "Cloud Security Compliance Audit",
- "description": "Automated compliance checking workflow that runs Prowler against your AWS infrastructure, compares findings against CIS benchmarks, generates a compliance report, and stores results as an artifact. Perfect for continuous compliance monitoring.",
- "category": "compliance",
- "tags": ["compliance", "aws", "prowler", "cis", "audit", "cloud"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Cloud Security Compliance Audit",
- "description": "Run Prowler compliance checks, analyze against CIS benchmarks, and generate reports.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "compliance",
- "tags": ["compliance", "aws", "prowler", "cis", "audit", "cloud"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "Cloud Security Compliance Audit",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start Audit", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "aws_creds",
- "type": "core.credentials.aws",
- "position": { "x": 350, "y": 250 },
- "data": {
- "label": "Load AWS Credentials",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "prowler_1",
- "type": "security.prowler.scan",
- "position": { "x": 600, "y": 250 },
- "data": {
- "label": "Run Prowler Scan",
- "config": {
- "params": { "checks": "cis_level1", "services": "iam,s3,ec2,rds,cloudtrail,kms" },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_analyze",
- "type": "core.logic.script",
- "position": { "x": 850, "y": 250 },
- "data": {
- "label": "Analyze Compliance",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_1",
- "type": "core.destination.artifact",
- "position": { "x": 1100, "y": 250 },
- "data": {
- "label": "Save Report Artifact",
- "config": { "params": { "filename": "compliance-report.json" }, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_1",
- "type": "core.notification.slack",
- "position": { "x": 1350, "y": 250 },
- "data": {
- "label": "Send Compliance Summary",
- "config": { "params": { "channel": "#compliance" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-aws_creds", "source": "trigger_1", "target": "aws_creds" },
- { "id": "aws_creds-prowler_1", "source": "aws_creds", "target": "prowler_1" },
- { "id": "prowler_1-script_analyze", "source": "prowler_1", "target": "script_analyze" },
- { "id": "script_analyze-artifact_1", "source": "script_analyze", "target": "artifact_1" },
- { "id": "artifact_1-notify_1", "source": "artifact_1", "target": "notify_1" }
- ]
- },
- "requiredSecrets": [
- {
- "name": "aws_access_key_id",
- "type": "string",
- "description": "AWS access key ID for the target account"
- },
- {
- "name": "aws_secret_access_key",
- "type": "string",
- "description": "AWS secret access key for the target account"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack webhook URL for compliance notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/cloud-compliance-check.json b/backend/scripts/seed-templates/cloud-compliance-check.json
deleted file mode 100644
index 56ccf30c..00000000
--- a/backend/scripts/seed-templates/cloud-compliance-check.json
+++ /dev/null
@@ -1,92 +0,0 @@
-{
- "_metadata": {
- "name": "Cloud Compliance Check",
- "description": "Infrastructure-as-Code compliance workflow that scans Terraform, CloudFormation, Kubernetes manifests, and Dockerfiles using Checkov for policy violations. A script node processes and summarizes findings by severity, then results are compiled into an artifact report and delivered via Slack. Supports CIS benchmarks, SOC 2, HIPAA, and PCI-DSS frameworks.",
- "category": "cloud-security",
- "tags": ["cloud", "compliance", "iac", "checkov", "devsecops"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Cloud Compliance Check",
- "description": "Scan IaC files for compliance violations, summarize findings, and deliver a compliance report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "cloud-security",
- "tags": ["cloud", "compliance", "iac", "checkov", "devsecops"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "Cloud Compliance Check",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start Compliance Check",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "checkov_1",
- "type": "sentris.checkov.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Scan IaC for Violations",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_summary",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Summarize Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Generate Compliance Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Notify Compliance Team",
- "config": { "params": { "channel": "#cloud-compliance" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-checkov_1", "source": "trigger_1", "target": "checkov_1" },
- { "id": "checkov_1-script_summary", "source": "checkov_1", "target": "script_summary" },
- {
- "id": "script_summary-artifact_report",
- "source": "script_summary",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for cloud compliance alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/cloud-iam-audit.json b/backend/scripts/seed-templates/cloud-iam-audit.json
deleted file mode 100644
index a880bb01..00000000
--- a/backend/scripts/seed-templates/cloud-iam-audit.json
+++ /dev/null
@@ -1,132 +0,0 @@
-{
- "_metadata": {
- "name": "Cloud IAM Audit",
- "description": "AWS IAM security audit using Prowler to detect overprivileged roles, unused credentials, missing MFA, and IAM policy violations. Findings are analyzed, compiled into an artifact report, and delivered to the security team via Slack.",
- "category": "cloud-security",
- "tags": [
- "aws",
- "iam",
- "prowler",
- "cloud-security",
- "audit",
- "mfa",
- "credentials",
- "privilege-escalation"
- ],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Cloud IAM Audit",
- "description": "Audit AWS IAM for overprivileged roles, unused credentials, and MFA gaps using Prowler.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "cloud-security",
- "tags": [
- "aws",
- "iam",
- "prowler",
- "cloud-security",
- "audit",
- "mfa",
- "credentials",
- "privilege-escalation"
- ],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "Cloud IAM Audit",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": {
- "label": "Start IAM Audit",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "aws_creds",
- "type": "core.credentials.aws",
- "position": { "x": 400, "y": 300 },
- "data": {
- "label": "Load AWS Credentials",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "prowler_iam",
- "type": "security.prowler.scan",
- "position": { "x": 700, "y": 300 },
- "data": {
- "label": "Prowler IAM Scan",
- "config": {
- "params": {
- "service": "iam",
- "severity": "high,critical"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_analyze",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 300 },
- "data": {
- "label": "Analyze IAM Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 300 },
- "data": {
- "label": "Generate IAM Audit Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_team",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 300 },
- "data": {
- "label": "Notify Cloud Security Team",
- "config": { "params": { "channel": "#cloud-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-aws_creds", "source": "trigger_1", "target": "aws_creds" },
- { "id": "aws_creds-prowler_iam", "source": "aws_creds", "target": "prowler_iam" },
- { "id": "prowler_iam-script_analyze", "source": "prowler_iam", "target": "script_analyze" },
- {
- "id": "script_analyze-artifact_report",
- "source": "script_analyze",
- "target": "artifact_report"
- },
- { "id": "artifact_report-notify_team", "source": "artifact_report", "target": "notify_team" }
- ]
- },
- "requiredSecrets": [
- {
- "name": "aws_access_key_id",
- "type": "string",
- "description": "AWS access key ID with IAM read permissions for Prowler scanning"
- },
- {
- "name": "aws_secret_access_key",
- "type": "string",
- "description": "AWS secret access key paired with the access key ID"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for cloud security audit notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/cloud-security-posture-audit.json b/backend/scripts/seed-templates/cloud-security-posture-audit.json
deleted file mode 100644
index 4f329fe5..00000000
--- a/backend/scripts/seed-templates/cloud-security-posture-audit.json
+++ /dev/null
@@ -1,118 +0,0 @@
-{
- "_metadata": {
- "name": "Cloud Security Posture Audit",
- "description": "Comprehensive cloud security audit that checks IAM policies, reviews S3 bucket configurations, scans for secret leaks with TruffleHog, and aggregates all findings into a unified security posture report. Use for periodic security reviews or pre-deployment checks.",
- "category": "security",
- "tags": ["cloud", "aws", "iam", "trufflehog", "secrets", "audit"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Cloud Security Posture Audit",
- "description": "Audit IAM policies, S3 buckets, and leaked secrets, then generate a unified posture report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "security",
- "tags": ["cloud", "aws", "iam", "trufflehog", "secrets", "audit"],
- "entryPoint": "trigger_1",
- "nodeCount": 7,
- "edgeCount": 8
- },
- "graph": {
- "name": "Cloud Security Posture Audit",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": { "label": "Start Posture Audit", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "aws_creds",
- "type": "core.credentials.aws",
- "position": { "x": 350, "y": 300 },
- "data": {
- "label": "Load AWS Credentials",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "http_iam",
- "type": "core.http.request",
- "position": { "x": 600, "y": 150 },
- "data": {
- "label": "Audit IAM Policies",
- "config": { "params": { "method": "GET" }, "inputOverrides": {} }
- }
- },
- {
- "id": "http_s3",
- "type": "core.http.request",
- "position": { "x": 600, "y": 300 },
- "data": {
- "label": "Check S3 Buckets",
- "config": { "params": { "method": "GET" }, "inputOverrides": {} }
- }
- },
- {
- "id": "trufflehog_1",
- "type": "sentris.trufflehog.scan",
- "position": { "x": 600, "y": 450 },
- "data": {
- "label": "Scan for Secret Leaks",
- "config": { "params": { "onlyVerified": true }, "inputOverrides": {} }
- }
- },
- {
- "id": "script_aggregate",
- "type": "core.logic.script",
- "position": { "x": 900, "y": 300 },
- "data": { "label": "Aggregate Findings", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "notify_report",
- "type": "core.notification.slack",
- "position": { "x": 1200, "y": 300 },
- "data": {
- "label": "Send Posture Report",
- "config": { "params": { "channel": "#security-posture" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-aws_creds", "source": "trigger_1", "target": "aws_creds" },
- { "id": "aws_creds-http_iam", "source": "aws_creds", "target": "http_iam" },
- { "id": "aws_creds-http_s3", "source": "aws_creds", "target": "http_s3" },
- { "id": "aws_creds-trufflehog_1", "source": "aws_creds", "target": "trufflehog_1" },
- { "id": "http_iam-script_aggregate", "source": "http_iam", "target": "script_aggregate" },
- { "id": "http_s3-script_aggregate", "source": "http_s3", "target": "script_aggregate" },
- {
- "id": "trufflehog_1-script_aggregate",
- "source": "trufflehog_1",
- "target": "script_aggregate"
- },
- {
- "id": "script_aggregate-notify_report",
- "source": "script_aggregate",
- "target": "notify_report"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "aws_access_key_id",
- "type": "string",
- "description": "AWS access key ID for the target account"
- },
- {
- "name": "aws_secret_access_key",
- "type": "string",
- "description": "AWS secret access key for the target account"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack webhook URL for posture report delivery"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/container-security-scan.json b/backend/scripts/seed-templates/container-security-scan.json
deleted file mode 100644
index f505129d..00000000
--- a/backend/scripts/seed-templates/container-security-scan.json
+++ /dev/null
@@ -1,111 +0,0 @@
-{
- "_metadata": {
- "name": "Container Security Scan",
- "description": "Automated container security workflow that accepts a Docker image reference, pulls image metadata via registry API, runs vulnerability analysis using Nuclei container templates, performs a compliance policy check, and generates an artifact report. Designed for CI/CD pipeline integration and pre-deployment security gates.",
- "category": "container-security",
- "tags": ["container", "docker", "kubernetes", "vulnerability", "compliance", "devsecops"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Container Security Scan",
- "description": "Scan container images for vulnerabilities, check compliance policies, and produce a security report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "container-security",
- "tags": ["container", "docker", "kubernetes", "vulnerability", "compliance", "devsecops"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "Container Security Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Image Submitted", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "http_registry",
- "type": "core.http.request",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Fetch Image Metadata",
- "config": { "params": { "method": "GET" }, "inputOverrides": {} }
- }
- },
- {
- "id": "nuclei_scan",
- "type": "sentris.nuclei.scan",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Scan Image Vulnerabilities",
- "config": {
- "params": { "severity": "medium,high,critical", "tags": "docker,cve" },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_compliance",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Check Compliance Policy",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Generate Security Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_result",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#container-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-http_registry", "source": "trigger_1", "target": "http_registry" },
- { "id": "http_registry-nuclei_scan", "source": "http_registry", "target": "nuclei_scan" },
- {
- "id": "nuclei_scan-script_compliance",
- "source": "nuclei_scan",
- "target": "script_compliance"
- },
- {
- "id": "script_compliance-artifact_report",
- "source": "script_compliance",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_result",
- "source": "artifact_report",
- "target": "notify_result"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "registry_token",
- "type": "string",
- "description": "Docker registry authentication token for pulling image metadata"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for container security alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/cve-impact-research-brief.json b/backend/scripts/seed-templates/cve-impact-research-brief.json
index 3c1e21a3..5f3e97b7 100644
--- a/backend/scripts/seed-templates/cve-impact-research-brief.json
+++ b/backend/scripts/seed-templates/cve-impact-research-brief.json
@@ -15,8 +15,8 @@
"category": "cve-research",
"tags": ["cve", "research", "nvd", "kev", "exploitability", "impact-analysis"],
"entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 9
+ "nodeCount": 5,
+ "edgeCount": 12
},
"graph": {
"name": "CVE Impact Research Brief",
@@ -25,7 +25,10 @@
{
"id": "trigger_1",
"type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 260 },
+ "position": {
+ "x": 100,
+ "y": 260
+ },
"data": {
"label": "CVE Research Input",
"config": {
@@ -66,34 +69,20 @@
}
},
{
- "id": "build_nvd_url",
- "type": "core.logic.script",
- "position": { "x": 420, "y": 160 },
- "data": {
- "label": "Build NVD Query URL",
- "config": {
- "params": {
- "variables": [{ "name": "cveId", "type": "string" }],
- "returns": [{ "name": "nvdUrl", "type": "string" }],
- "code": "export function script(input) {\n const cveId = String(input.cveId || '').trim().toUpperCase();\n return { nvdUrl: `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${encodeURIComponent(cveId)}` };\n}"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "fetch_nvd",
- "type": "core.http.request",
- "position": { "x": 740, "y": 160 },
+ "id": "query_nvd",
+ "type": "sentris.nvd.cve.query",
+ "position": {
+ "x": 580,
+ "y": 160
+ },
"data": {
- "label": "Fetch NVD CVE Metadata",
+ "label": "Query NVD CVE Metadata",
"config": {
"params": {
- "method": "GET",
- "contentType": "application/json",
- "authType": "none",
- "timeout": 30000,
- "failOnError": false
+ "resultsPerPage": 20,
+ "includeRejected": false,
+ "timeoutMs": 30000,
+ "failOnUnavailable": false
},
"inputOverrides": {}
}
@@ -102,7 +91,10 @@
{
"id": "fetch_kev",
"type": "core.http.request",
- "position": { "x": 740, "y": 360 },
+ "position": {
+ "x": 740,
+ "y": 360
+ },
"data": {
"label": "Fetch CISA KEV Catalog",
"config": {
@@ -122,21 +114,63 @@
{
"id": "assemble_research_brief",
"type": "core.logic.script",
- "position": { "x": 1060, "y": 260 },
+ "position": {
+ "x": 1060,
+ "y": 260
+ },
"data": {
"label": "Assemble Research Brief",
"config": {
"params": {
"variables": [
- { "name": "cveId", "type": "string" },
- { "name": "nvdData", "type": "json" },
- { "name": "kevData", "type": "json" },
- { "name": "product", "type": "string" },
- { "name": "version", "type": "string" },
- { "name": "deploymentNotes", "type": "string" }
+ {
+ "name": "cveId",
+ "type": "string"
+ },
+ {
+ "name": "nvdData",
+ "type": "json"
+ },
+ {
+ "name": "nvdStatus",
+ "type": "number"
+ },
+ {
+ "name": "nvdStatusText",
+ "type": "string"
+ },
+ {
+ "name": "kevData",
+ "type": "json"
+ },
+ {
+ "name": "kevStatus",
+ "type": "number"
+ },
+ {
+ "name": "kevStatusText",
+ "type": "string"
+ },
+ {
+ "name": "product",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "type": "string"
+ },
+ {
+ "name": "deploymentNotes",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "brief",
+ "type": "json"
+ }
],
- "returns": [{ "name": "brief", "type": "json" }],
- "code": "export function script(input) {\n const cveId = String(input.cveId || '').trim().toUpperCase();\n const nvdItems = Array.isArray(input.nvdData?.vulnerabilities) ? input.nvdData.vulnerabilities : [];\n const cve = nvdItems[0]?.cve || {};\n const descriptions = Array.isArray(cve.descriptions) ? cve.descriptions : [];\n const description = descriptions.find((item) => item.lang === 'en')?.value || descriptions[0]?.value || 'No NVD description available.';\n const references = Array.isArray(cve.references?.referenceData) ? cve.references.referenceData : Array.isArray(cve.references) ? cve.references : [];\n const kevItems = Array.isArray(input.kevData?.vulnerabilities) ? input.kevData.vulnerabilities : [];\n const kev = kevItems.find((item) => String(item.cveID || '').toUpperCase() === cveId) || null;\n const metrics = cve.metrics || {};\n return { brief: { cveId, product: input.product || null, version: input.version || null, deploymentNotes: input.deploymentNotes || null, description, nvdPublished: cve.published || null, nvdLastModified: cve.lastModified || null, metrics, knownExploited: Boolean(kev), kev, references: references.slice(0, 20), nextSteps: ['Confirm whether the affected product and version are present in authorized scope', 'Review vendor advisory and patch guidance', 'Design non-destructive validation checks', 'Create detection notes for exposed affected assets'] } };\n}"
+ "code": "export function script(input) {\n const cveId = String(input.cveId || '').trim().toUpperCase();\n const nvdStatus = Number(input.nvdStatus || 0);\n const kevStatus = Number(input.kevStatus || 0);\n const dataSources = {\n nvd: { status: nvdStatus, statusText: input.nvdStatusText || null, ok: nvdStatus >= 200 && nvdStatus < 300 },\n cisaKev: { status: kevStatus, statusText: input.kevStatusText || null, ok: kevStatus >= 200 && kevStatus < 300 }\n };\n const warnings = [];\n if (!dataSources.nvd.ok) warnings.push(`NVD enrichment unavailable: ${dataSources.nvd.statusText || dataSources.nvd.status || 'unknown error'}`);\n if (!dataSources.cisaKev.ok) warnings.push(`CISA KEV enrichment unavailable: ${dataSources.cisaKev.statusText || dataSources.cisaKev.status || 'unknown error'}`);\n const nvdItems = dataSources.nvd.ok && Array.isArray(input.nvdData?.vulnerabilities) ? input.nvdData.vulnerabilities : [];\n const cve = nvdItems[0]?.cve || {};\n const descriptions = Array.isArray(cve.descriptions) ? cve.descriptions : [];\n const description = descriptions.find((item) => item.lang === 'en')?.value || descriptions[0]?.value || 'No NVD description available.';\n const references = Array.isArray(cve.references?.referenceData) ? cve.references.referenceData : Array.isArray(cve.references) ? cve.references : [];\n const kevItems = dataSources.cisaKev.ok && Array.isArray(input.kevData?.vulnerabilities) ? input.kevData.vulnerabilities : [];\n const kev = kevItems.find((item) => String(item.cveID || '').toUpperCase() === cveId) || null;\n const metrics = cve.metrics || {};\n return { brief: { cveId, product: input.product || null, version: input.version || null, deploymentNotes: input.deploymentNotes || null, dataSources, warnings, description, nvdPublished: cve.published || null, nvdLastModified: cve.lastModified || null, metrics, knownExploited: Boolean(kev), kev, references: references.slice(0, 20), nextSteps: ['Confirm whether the affected product and version are present in authorized scope', 'Review vendor advisory and patch guidance', 'Design non-destructive validation checks', 'Create detection notes for exposed affected assets'] } };\n}"
},
"inputOverrides": {}
}
@@ -145,7 +179,10 @@
{
"id": "artifact_report",
"type": "core.artifact.writer",
- "position": { "x": 1380, "y": 260 },
+ "position": {
+ "x": 1380,
+ "y": 260
+ },
"data": {
"label": "Save CVE Research Brief",
"config": {
@@ -164,18 +201,11 @@
],
"edges": [
{
- "id": "trigger_1-build_nvd_url-cveId",
+ "id": "trigger_1-query_nvd-cveId",
"source": "trigger_1",
- "target": "build_nvd_url",
+ "target": "query_nvd",
"sourceHandle": "cveId",
- "targetHandle": "cveId"
- },
- {
- "id": "build_nvd_url-fetch_nvd-nvdUrl",
- "source": "build_nvd_url",
- "target": "fetch_nvd",
- "sourceHandle": "nvdUrl",
- "targetHandle": "url"
+ "targetHandle": "cveIds"
},
{
"id": "trigger_1-assemble_research_brief-cveId",
@@ -206,12 +236,26 @@
"targetHandle": "deploymentNotes"
},
{
- "id": "fetch_nvd-assemble_research_brief-data",
- "source": "fetch_nvd",
+ "id": "query_nvd-assemble_research_brief-data",
+ "source": "query_nvd",
"target": "assemble_research_brief",
"sourceHandle": "data",
"targetHandle": "nvdData"
},
+ {
+ "id": "query_nvd-assemble_research_brief-status",
+ "source": "query_nvd",
+ "target": "assemble_research_brief",
+ "sourceHandle": "status",
+ "targetHandle": "nvdStatus"
+ },
+ {
+ "id": "query_nvd-assemble_research_brief-statusText",
+ "source": "query_nvd",
+ "target": "assemble_research_brief",
+ "sourceHandle": "statusText",
+ "targetHandle": "nvdStatusText"
+ },
{
"id": "fetch_kev-assemble_research_brief-data",
"source": "fetch_kev",
@@ -219,6 +263,20 @@
"sourceHandle": "data",
"targetHandle": "kevData"
},
+ {
+ "id": "fetch_kev-assemble_research_brief-status",
+ "source": "fetch_kev",
+ "target": "assemble_research_brief",
+ "sourceHandle": "status",
+ "targetHandle": "kevStatus"
+ },
+ {
+ "id": "fetch_kev-assemble_research_brief-statusText",
+ "source": "fetch_kev",
+ "target": "assemble_research_brief",
+ "sourceHandle": "statusText",
+ "targetHandle": "kevStatusText"
+ },
{
"id": "assemble_research_brief-artifact_report-brief",
"source": "assemble_research_brief",
diff --git a/backend/scripts/seed-templates/dns-bruteforce-scan.json b/backend/scripts/seed-templates/dns-bruteforce-scan.json
deleted file mode 100644
index 4ac2d20e..00000000
--- a/backend/scripts/seed-templates/dns-bruteforce-scan.json
+++ /dev/null
@@ -1,99 +0,0 @@
-{
- "_metadata": {
- "name": "DNS Bruteforce Scan",
- "description": "Subdomain discovery workflow using ShuffleDNS for high-speed DNS brute-forcing with massdns as the resolver backend. Enumerates subdomains against a wordlist, formats results into a structured discovery report, and delivers findings via Slack. Ideal for expanding attack surface visibility beyond passive enumeration.",
- "category": "dns-security",
- "tags": ["dns", "bruteforce", "subdomain", "shuffledns", "massdns", "enumeration"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "DNS Bruteforce Scan",
- "description": "Brute-force subdomains with ShuffleDNS, format results, and deliver a discovery report via Slack.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "dns-security",
- "tags": ["dns", "bruteforce", "subdomain", "shuffledns", "massdns", "enumeration"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "DNS Bruteforce Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start DNS Bruteforce",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "shuffledns_1",
- "type": "sentris.shuffledns.massdns",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Bruteforce Subdomains",
- "config": {
- "params": {},
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "format_results",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Format Discovery Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Save Bruteforce Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#dns-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-shuffledns_1", "source": "trigger_1", "target": "shuffledns_1" },
- {
- "id": "shuffledns_1-format_results",
- "source": "shuffledns_1",
- "target": "format_results"
- },
- {
- "id": "format_results-artifact_report",
- "source": "format_results",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for DNS bruteforce scan alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/dns-security-scan.json b/backend/scripts/seed-templates/dns-security-scan.json
deleted file mode 100644
index fee55b38..00000000
--- a/backend/scripts/seed-templates/dns-security-scan.json
+++ /dev/null
@@ -1,107 +0,0 @@
-{
- "_metadata": {
- "name": "DNS Security Scan",
- "description": "Comprehensive DNS security assessment that enumerates subdomains with Subfinder, resolves all DNS record types via DNSX (A, AAAA, CNAME, MX, NS, TXT, SOA), detects dangling DNS records pointing to deprovisioned resources, and checks for potential zone transfer misconfigurations. Produces a consolidated security report and delivers it via Slack.",
- "category": "dns-security",
- "tags": ["dns", "dnsx", "subfinder", "dangling-dns", "zone-transfer", "enumeration"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "DNS Security Scan",
- "description": "Enumerate DNS records, detect dangling DNS entries and zone transfer issues, and generate a security report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "dns-security",
- "tags": ["dns", "dnsx", "subfinder", "dangling-dns", "zone-transfer", "enumeration"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "DNS Security Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start DNS Security Scan",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "subfinder_1",
- "type": "sentris.subfinder.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Enumerate Subdomains",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "dnsx_resolve",
- "type": "sentris.dnsx.run",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Resolve All DNS Records",
- "config": {
- "params": {
- "recordTypes": ["A", "AAAA", "CNAME", "MX", "NS", "TXT", "SOA"]
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_analyze",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Detect Dangling DNS & Zone Transfer Issues",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Generate DNS Security Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#dns-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-subfinder_1", "source": "trigger_1", "target": "subfinder_1" },
- { "id": "subfinder_1-dnsx_resolve", "source": "subfinder_1", "target": "dnsx_resolve" },
- { "id": "dnsx_resolve-script_analyze", "source": "dnsx_resolve", "target": "script_analyze" },
- {
- "id": "script_analyze-artifact_report",
- "source": "script_analyze",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for DNS security scan notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/email-header-analysis.json b/backend/scripts/seed-templates/email-header-analysis.json
deleted file mode 100644
index 537692eb..00000000
--- a/backend/scripts/seed-templates/email-header-analysis.json
+++ /dev/null
@@ -1,107 +0,0 @@
-{
- "_metadata": {
- "name": "Email Header Analysis",
- "description": "Email authentication verification workflow that checks SPF, DKIM, and DMARC DNS records for a target domain using DNSX. Validates record syntax, evaluates policy strictness, identifies misconfigurations (e.g., permissive SPF ~all, missing DMARC reject policy), and scores overall email security posture. Produces an actionable compliance report with remediation recommendations.",
- "category": "email-security",
- "tags": ["email", "spf", "dkim", "dmarc", "dns", "authentication"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Email Header Analysis",
- "description": "Verify SPF, DKIM, and DMARC records for a domain, evaluate email authentication posture, and generate a compliance report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "email-security",
- "tags": ["email", "spf", "dkim", "dmarc", "dns", "authentication"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "Email Header Analysis",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start Email Auth Check",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "dnsx_email",
- "type": "sentris.dnsx.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Query SPF, DKIM & DMARC Records",
- "config": {
- "params": {
- "recordTypes": ["TXT", "MX", "CNAME"]
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_parse",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Parse & Validate Email Auth Records",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_score",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Score Email Security Posture",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Generate Email Auth Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#email-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-dnsx_email", "source": "trigger_1", "target": "dnsx_email" },
- { "id": "dnsx_email-script_parse", "source": "dnsx_email", "target": "script_parse" },
- { "id": "script_parse-script_score", "source": "script_parse", "target": "script_score" },
- {
- "id": "script_score-artifact_report",
- "source": "script_score",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for email security analysis notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/employee-offboarding-security.json b/backend/scripts/seed-templates/employee-offboarding-security.json
deleted file mode 100644
index 33876efc..00000000
--- a/backend/scripts/seed-templates/employee-offboarding-security.json
+++ /dev/null
@@ -1,178 +0,0 @@
-{
- "_metadata": {
- "name": "Employee Offboarding Security",
- "description": "Human-in-the-loop IT security workflow for employee departures. A manual approval gate kicks off parallel account deprovisioning across Okta, GitHub, and Google Workspace, then verifies completion and sends a confirmation to the security team via Slack.",
- "category": "automation",
- "tags": [
- "offboarding",
- "okta",
- "github",
- "google-workspace",
- "manual-approval",
- "it-automation",
- "human-in-the-loop"
- ],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Employee Offboarding Security",
- "description": "Approve and execute parallel account deprovisioning across Okta, GitHub, and Google Workspace on employee departure.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "automation",
- "tags": [
- "offboarding",
- "okta",
- "github",
- "google-workspace",
- "manual-approval",
- "it-automation",
- "human-in-the-loop"
- ],
- "entryPoint": "trigger_1",
- "nodeCount": 8,
- "edgeCount": 9
- },
- "graph": {
- "name": "Employee Offboarding Security",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": {
- "label": "Offboarding Request Received",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "manual_approve",
- "type": "core.manual_action.approval",
- "position": { "x": 400, "y": 300 },
- "data": {
- "label": "Manager Approval",
- "config": {
- "params": {
- "title": "Employee Offboarding Approval",
- "message": "Please review and approve the employee offboarding request. This will suspend accounts across Okta, GitHub, and Google Workspace."
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "okta_suspend",
- "type": "it-automation.okta.user-offboard",
- "position": { "x": 750, "y": 100 },
- "data": {
- "label": "Suspend Okta Account",
- "config": {
- "params": {
- "action": "suspend"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "github_remove",
- "type": "github.org.membership.remove",
- "position": { "x": 750, "y": 300 },
- "data": {
- "label": "Remove GitHub Org Access",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "google_unassign",
- "type": "it-automation.google-workspace.user-delete",
- "position": { "x": 750, "y": 500 },
- "data": {
- "label": "Unassign Google Workspace License",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_verify",
- "type": "core.logic.script",
- "position": { "x": 1100, "y": 300 },
- "data": {
- "label": "Verify Deprovisioning",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_audit",
- "type": "core.artifact.writer",
- "position": { "x": 1400, "y": 300 },
- "data": {
- "label": "Save Offboarding Audit Log",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_confirm",
- "type": "core.notification.slack",
- "position": { "x": 1700, "y": 300 },
- "data": {
- "label": "Confirm Offboarding Complete",
- "config": { "params": { "channel": "#it-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-manual_approve", "source": "trigger_1", "target": "manual_approve" },
- { "id": "manual_approve-okta_suspend", "source": "manual_approve", "target": "okta_suspend" },
- {
- "id": "manual_approve-github_remove",
- "source": "manual_approve",
- "target": "github_remove"
- },
- {
- "id": "manual_approve-google_unassign",
- "source": "manual_approve",
- "target": "google_unassign"
- },
- { "id": "okta_suspend-script_verify", "source": "okta_suspend", "target": "script_verify" },
- { "id": "github_remove-script_verify", "source": "github_remove", "target": "script_verify" },
- {
- "id": "google_unassign-script_verify",
- "source": "google_unassign",
- "target": "script_verify"
- },
- {
- "id": "script_verify-artifact_audit",
- "source": "script_verify",
- "target": "artifact_audit"
- },
- {
- "id": "artifact_audit-notify_confirm",
- "source": "artifact_audit",
- "target": "notify_confirm"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "okta_api_token",
- "type": "string",
- "description": "Okta API token for user account suspension and deprovisioning"
- },
- {
- "name": "github_token",
- "type": "string",
- "description": "GitHub Personal Access Token with org:admin scope for membership removal"
- },
- {
- "name": "google_workspace_credentials",
- "type": "string",
- "description": "Google Workspace service account credentials JSON for license management"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for offboarding confirmation notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/exposed-service-cve-mapper.json b/backend/scripts/seed-templates/exposed-service-cve-mapper.json
index 1c88b11e..b61c6f1f 100644
--- a/backend/scripts/seed-templates/exposed-service-cve-mapper.json
+++ b/backend/scripts/seed-templates/exposed-service-cve-mapper.json
@@ -16,7 +16,7 @@
"tags": ["cve", "service-fingerprinting", "naabu", "httpx", "exposure", "nvd"],
"entryPoint": "trigger_1",
"nodeCount": 8,
- "edgeCount": 8
+ "edgeCount": 10
},
"graph": {
"name": "Exposed Service CVE Mapper",
@@ -25,7 +25,10 @@
{
"id": "trigger_1",
"type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 260 },
+ "position": {
+ "x": 100,
+ "y": 260
+ },
"data": {
"label": "Authorized Targets Input",
"config": {
@@ -54,7 +57,10 @@
{
"id": "naabu_ports",
"type": "sentris.naabu.scan",
- "position": { "x": 420, "y": 260 },
+ "position": {
+ "x": 420,
+ "y": 260
+ },
"data": {
"label": "Discover Exposed Ports",
"config": {
@@ -71,13 +77,26 @@
{
"id": "extract_http_targets",
"type": "core.logic.script",
- "position": { "x": 740, "y": 260 },
+ "position": {
+ "x": 740,
+ "y": 260
+ },
"data": {
"label": "Build HTTP Probe Targets",
"config": {
"params": {
- "variables": [{ "name": "openPorts", "type": "list-json" }],
- "returns": [{ "name": "httpTargets", "type": "list-text" }],
+ "variables": [
+ {
+ "name": "openPorts",
+ "type": "list-json"
+ }
+ ],
+ "returns": [
+ {
+ "name": "httpTargets",
+ "type": "list-text"
+ }
+ ],
"code": "export function script(input) {\n const ports = Array.isArray(input.openPorts) ? input.openPorts : [];\n const urls = [];\n for (const item of ports) {\n const host = item?.host || item?.ip;\n const port = Number(item?.port);\n if (!host || !Number.isFinite(port)) continue;\n if ([443, 8443, 9443].includes(port)) urls.push(`https://${host}:${port}`);\n else if ([80, 8080, 8000, 8888, 3000, 5000, 7001, 9000].includes(port)) urls.push(`http://${host}:${port}`);\n else urls.push(`http://${host}:${port}`, `https://${host}:${port}`);\n }\n return { httpTargets: Array.from(new Set(urls)) };\n}"
},
"inputOverrides": {}
@@ -87,7 +106,10 @@
{
"id": "httpx_fingerprint",
"type": "sentris.httpx.scan",
- "position": { "x": 1060, "y": 260 },
+ "position": {
+ "x": 1060,
+ "y": 260
+ },
"data": {
"label": "Fingerprint HTTP Services",
"config": {
@@ -104,35 +126,51 @@
{
"id": "build_cve_queries",
"type": "core.logic.script",
- "position": { "x": 1380, "y": 160 },
+ "position": {
+ "x": 1380,
+ "y": 160
+ },
"data": {
- "label": "Build CVE Search Query",
+ "label": "Build CVE Search Keyword",
"config": {
"params": {
- "variables": [{ "name": "httpResponses", "type": "list-json" }],
+ "variables": [
+ {
+ "name": "httpResponses",
+ "type": "list-json"
+ }
+ ],
"returns": [
- { "name": "nvdUrl", "type": "string" },
- { "name": "fingerprints", "type": "json" }
+ {
+ "name": "keywordSearch",
+ "type": "string"
+ },
+ {
+ "name": "fingerprints",
+ "type": "json"
+ }
],
- "code": "export function script(input) {\n const responses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\n const tech = Array.from(new Set(responses.flatMap((item) => Array.isArray(item?.technologies) ? item.technologies : []).map(String).filter(Boolean)));\n const titles = Array.from(new Set(responses.map((item) => item?.title).filter((value) => typeof value === 'string' && value.length > 0)));\n const keyword = tech[0] || titles[0] || 'web server';\n const nvdUrl = `https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${encodeURIComponent(keyword)}`;\n return { nvdUrl, fingerprints: { keyword, technologies: tech, titles, services: responses.map((item) => ({ url: item.url, statusCode: item.statusCode, title: item.title, technologies: item.technologies || [] })) } };\n}"
+ "code": "export function script(input) {\n const responses = Array.isArray(input.httpResponses) ? input.httpResponses : [];\n function normalizeKeyword(value) {\n const text = String(value || '').replace(/\\s+/g, ' ').trim();\n if (!text) return '';\n const stripped = text.replace(/[:/\\s-]*v?\\d+(?:\\.\\d+){1,}(?:[._~+-][A-Za-z0-9]+)?(?:\\b.*)?$/i, '').trim();\n return stripped.length >= 3 ? stripped : text;\n }\n function isLowSignalKeyword(value) {\n return /^(ubuntu|debian|linux|windows|centos|red hat|fedora|freebsd)$/i.test(value);\n }\n const rawTechnologies = Array.from(new Set(responses.flatMap((item) => Array.isArray(item?.technologies) ? item.technologies : []).map(String).filter(Boolean)));\n const tech = Array.from(new Set(rawTechnologies.map(normalizeKeyword).filter(Boolean)));\n const titles = Array.from(new Set(responses.map((item) => item?.title).filter((value) => typeof value === 'string' && value.length > 0)));\n const keywordSearch = tech.find((item) => !isLowSignalKeyword(item)) || tech[0] || normalizeKeyword(titles[0]) || 'web server';\n return { keywordSearch, fingerprints: { keyword: keywordSearch, technologies: tech, rawTechnologies, titles, services: responses.map((item) => ({ url: item.url, statusCode: item.statusCode, title: item.title, technologies: item.technologies || [] })) } };\n}"
},
"inputOverrides": {}
}
}
},
{
- "id": "fetch_nvd_candidates",
- "type": "core.http.request",
- "position": { "x": 1700, "y": 160 },
+ "id": "query_nvd_candidates",
+ "type": "sentris.nvd.cve.query",
+ "position": {
+ "x": 1700,
+ "y": 160
+ },
"data": {
- "label": "Fetch Candidate CVEs",
+ "label": "Query Candidate CVEs in NVD",
"config": {
"params": {
- "method": "GET",
- "contentType": "application/json",
- "authType": "none",
- "timeout": 30000,
- "failOnError": false
+ "resultsPerPage": 5,
+ "includeRejected": false,
+ "timeoutMs": 90000,
+ "failOnUnavailable": false
},
"inputOverrides": {}
}
@@ -141,17 +179,39 @@
{
"id": "rank_cve_candidates",
"type": "core.logic.script",
- "position": { "x": 2020, "y": 260 },
+ "position": {
+ "x": 2020,
+ "y": 260
+ },
"data": {
"label": "Rank Candidate CVEs",
"config": {
"params": {
"variables": [
- { "name": "fingerprints", "type": "json" },
- { "name": "nvdData", "type": "json" }
+ {
+ "name": "fingerprints",
+ "type": "json"
+ },
+ {
+ "name": "nvdData",
+ "type": "json"
+ },
+ {
+ "name": "nvdStatus",
+ "type": "number"
+ },
+ {
+ "name": "nvdStatusText",
+ "type": "string"
+ }
],
- "returns": [{ "name": "report", "type": "json" }],
- "code": "export function script(input) {\n const vulnerabilities = Array.isArray(input.nvdData?.vulnerabilities) ? input.nvdData.vulnerabilities : [];\n const candidates = vulnerabilities.slice(0, 50).map((item) => ({ id: item?.cve?.id, published: item?.cve?.published, lastModified: item?.cve?.lastModified, descriptions: item?.cve?.descriptions, metrics: item?.cve?.metrics, references: item?.cve?.references }));\n return { report: { summary: { fingerprintKeyword: input.fingerprints?.keyword || null, technologiesObserved: input.fingerprints?.technologies || [], servicesObserved: Array.isArray(input.fingerprints?.services) ? input.fingerprints.services.length : 0, candidateCves: candidates.length }, services: input.fingerprints?.services || [], candidates, nextSteps: ['Verify exact product and version before reporting impact', 'Prioritize externally reachable services with confirmed technology matches', 'Use only non-destructive validation inside authorized scope'] } };\n}"
+ "returns": [
+ {
+ "name": "report",
+ "type": "json"
+ }
+ ],
+ "code": "export function script(input) {\n const nvdStatus = Number(input.nvdStatus || 0);\n const dataSources = { nvd: { status: nvdStatus, statusText: input.nvdStatusText || null, ok: nvdStatus >= 200 && nvdStatus < 300 } };\n const warnings = [];\n if (!dataSources.nvd.ok) warnings.push('NVD candidate lookup unavailable: ' + (dataSources.nvd.statusText || dataSources.nvd.status || 'unknown error'));\n const fingerprints = input.fingerprints || {};\n const keyword = typeof fingerprints.keyword === 'string' ? fingerprints.keyword.trim() : '';\n const technologies = Array.isArray(fingerprints.technologies) ? fingerprints.technologies.map(String).map((item) => item.trim()).filter(Boolean) : [];\n const vulnerabilities = dataSources.nvd.ok && Array.isArray(input.nvdData?.vulnerabilities) ? input.nvdData.vulnerabilities : [];\n const normalize = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\\s+/g, ' ').trim();\n const contains = (haystack, needle) => { const normalizedNeedle = normalize(needle); return normalizedNeedle ? normalize(haystack).includes(normalizedNeedle) : false; };\n const description = (cve) => { const descriptions = Array.isArray(cve?.descriptions) ? cve.descriptions : []; const english = descriptions.find((item) => item?.lang === 'en' && typeof item.value === 'string'); const fallback = descriptions.find((item) => typeof item?.value === 'string'); return english?.value || fallback?.value || ''; };\n const severityRank = (severity) => ({ CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, NONE: 0 })[String(severity || '').toUpperCase()] || 0;\n const severityFromScore = (score) => !Number.isFinite(score) ? null : score >= 9 ? 'CRITICAL' : score >= 7 ? 'HIGH' : score >= 4 ? 'MEDIUM' : score > 0 ? 'LOW' : 'NONE';\n const extractCvss = (metrics) => { const scores = []; for (const key of ['cvssMetricV40', 'cvssMetricV31', 'cvssMetricV30', 'cvssMetricV2']) { const entries = Array.isArray(metrics?.[key]) ? metrics[key] : []; for (const entry of entries) { const score = Number(entry?.cvssData?.baseScore); if (!Number.isFinite(score)) continue; const severity = String(entry?.cvssData?.baseSeverity || entry?.baseSeverity || severityFromScore(score) || '').toUpperCase(); scores.push({ score, severity }); } } scores.sort((a, b) => severityRank(b.severity) - severityRank(a.severity) || b.score - a.score); return scores[0] || { score: null, severity: null }; };\n const referencesFor = (references) => Array.isArray(references) ? references : Array.isArray(references?.referenceData) ? references.referenceData : [];\n const parseDate = (value) => { const parsed = Date.parse(value || ''); return Number.isFinite(parsed) ? parsed : 0; };\n const recencyBoost = (cve) => { const newest = Math.max(parseDate(cve?.lastModified), parseDate(cve?.published)); if (!newest) return { score: 0, reason: null }; const ageDays = Math.max(0, (Date.now() - newest) / 86400000); if (ageDays <= 730) return { score: 10, reason: 'recently modified or published' }; if (ageDays <= 1825) return { score: 5, reason: 'modified or published in the last five years' }; return { score: 0, reason: null }; };\n const candidates = vulnerabilities.map((item) => {\n const cve = item?.cve || {};\n const references = referencesFor(cve.references);\n const text = [cve.id, description(cve), references.map((reference) => reference?.url || reference?.name || reference?.source || '').join(' ')].join(' ');\n const cvss = extractCvss(cve.metrics || {});\n const reasons = [];\n let priorityScore = 0;\n if (keyword && contains(text, keyword)) { priorityScore += 45; reasons.push('matches fingerprint keyword \"' + keyword + '\"'); }\n const matchedTechnologies = technologies.filter((technology) => technology !== keyword && contains(text, technology)).slice(0, 3);\n if (matchedTechnologies.length > 0) { priorityScore += matchedTechnologies.length * 12; reasons.push('matches observed technologies: ' + matchedTechnologies.join(', ')); }\n if (Number.isFinite(cvss.score)) { priorityScore += cvss.score * 5; const rank = severityRank(cvss.severity); if (rank >= 4) priorityScore += 15; else if (rank === 3) priorityScore += 10; else if (rank === 2) priorityScore += 5; reasons.push(String(cvss.severity || 'unknown').toLowerCase() + ' severity (CVSS ' + cvss.score + ')'); }\n const recency = recencyBoost(cve);\n if (recency.score > 0) { priorityScore += recency.score; reasons.push(recency.reason); }\n if (references.some((reference) => /exploit|metasploit|github\\.com|packetstorm|nuclei/i.test(String(reference?.url || reference?.name || reference?.source || '')))) { priorityScore += 6; reasons.push('has exploit or proof-of-concept style references'); }\n return { id: cve.id, published: cve.published, lastModified: cve.lastModified, severity: cvss.severity, cvssScore: cvss.score, priorityScore: Math.round(priorityScore * 10) / 10, priorityReasons: reasons.length > 0 ? reasons : ['no direct fingerprint match; review manually'], descriptions: cve.descriptions, metrics: cve.metrics, references: cve.references };\n }).sort((a, b) => b.priorityScore - a.priorityScore || Number(b.cvssScore || 0) - Number(a.cvssScore || 0) || parseDate(b.lastModified || b.published) - parseDate(a.lastModified || a.published)).slice(0, 50);\n const highestSeverity = candidates.reduce((current, candidate) => severityRank(candidate.severity) > severityRank(current) ? candidate.severity : current, null);\n return { report: { summary: { fingerprintKeyword: keyword, technologiesObserved: technologies, servicesObserved: Array.isArray(fingerprints.services) ? fingerprints.services.length : 0, candidateCves: candidates.length, topCandidate: candidates[0]?.id || null, highestSeverity }, dataSources, warnings, services: fingerprints.services || [], candidates, nextSteps: ['Verify exact product and version before reporting impact', 'Prioritize externally reachable services with confirmed technology matches', 'Use only non-destructive validation inside authorized scope'] } };\n}"
},
"inputOverrides": {}
}
@@ -160,7 +220,10 @@
{
"id": "artifact_report",
"type": "core.artifact.writer",
- "position": { "x": 2340, "y": 260 },
+ "position": {
+ "x": 2340,
+ "y": 260
+ },
"data": {
"label": "Save CVE Candidate Map",
"config": {
@@ -207,11 +270,11 @@
"targetHandle": "httpResponses"
},
{
- "id": "build_cve_queries-fetch_nvd_candidates-nvdUrl",
+ "id": "build_cve_queries-query_nvd_candidates-keywordSearch",
"source": "build_cve_queries",
- "target": "fetch_nvd_candidates",
- "sourceHandle": "nvdUrl",
- "targetHandle": "url"
+ "target": "query_nvd_candidates",
+ "sourceHandle": "keywordSearch",
+ "targetHandle": "keywordSearch"
},
{
"id": "build_cve_queries-rank_cve_candidates-fingerprints",
@@ -221,12 +284,26 @@
"targetHandle": "fingerprints"
},
{
- "id": "fetch_nvd_candidates-rank_cve_candidates-data",
- "source": "fetch_nvd_candidates",
+ "id": "query_nvd_candidates-rank_cve_candidates-data",
+ "source": "query_nvd_candidates",
"target": "rank_cve_candidates",
"sourceHandle": "data",
"targetHandle": "nvdData"
},
+ {
+ "id": "query_nvd_candidates-rank_cve_candidates-status",
+ "source": "query_nvd_candidates",
+ "target": "rank_cve_candidates",
+ "sourceHandle": "status",
+ "targetHandle": "nvdStatus"
+ },
+ {
+ "id": "query_nvd_candidates-rank_cve_candidates-statusText",
+ "source": "query_nvd_candidates",
+ "target": "rank_cve_candidates",
+ "sourceHandle": "statusText",
+ "targetHandle": "nvdStatusText"
+ },
{
"id": "rank_cve_candidates-artifact_report-report",
"source": "rank_cve_candidates",
diff --git a/backend/scripts/seed-templates/github-repo-secret-scan.json b/backend/scripts/seed-templates/github-repo-secret-scan.json
deleted file mode 100644
index 7b8c436e..00000000
--- a/backend/scripts/seed-templates/github-repo-secret-scan.json
+++ /dev/null
@@ -1,91 +0,0 @@
-{
- "_metadata": {
- "name": "GitHub Repo Secret Scan",
- "description": "Automated secret detection workflow that scans a GitHub repository for exposed credentials, API keys, and other secrets using TruffleHog. Includes issue and PR comment scanning for thorough coverage, evaluates findings against verification status, and generates an artifact report. Designed for DevSecOps pipelines and repository hygiene audits.",
- "category": "secret-detection",
- "tags": ["secrets", "trufflehog", "github", "credential-scanning", "devsecops"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "GitHub Repo Secret Scan",
- "description": "Scan a GitHub repository for leaked secrets and credentials using TruffleHog, evaluate findings, and produce a security report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "secret-detection",
- "tags": ["secrets", "trufflehog", "github", "credential-scanning", "devsecops"],
- "entryPoint": "trigger_1",
- "nodeCount": 4,
- "edgeCount": 3
- },
- "graph": {
- "name": "GitHub Repo Secret Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "GitHub Repo Submitted",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "trufflehog_scan",
- "type": "sentris.trufflehog.scan",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Scan GitHub Repo for Secrets",
- "config": {
- "params": {
- "scanType": "github",
- "onlyVerified": true,
- "jsonOutput": true,
- "includeIssueComments": true,
- "includePRComments": true
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_evaluate",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Evaluate Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Generate Secret Scan Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-trufflehog_scan", "source": "trigger_1", "target": "trufflehog_scan" },
- {
- "id": "trufflehog_scan-script_evaluate",
- "source": "trufflehog_scan",
- "target": "script_evaluate"
- },
- {
- "id": "script_evaluate-artifact_report",
- "source": "script_evaluate",
- "target": "artifact_report"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "github_token",
- "type": "string",
- "description": "GitHub Personal Access Token for repository access and scanning"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/github-repo-vulnerability-scan.json b/backend/scripts/seed-templates/github-repo-vulnerability-scan.json
deleted file mode 100644
index 2a7e2dd7..00000000
--- a/backend/scripts/seed-templates/github-repo-vulnerability-scan.json
+++ /dev/null
@@ -1,108 +0,0 @@
-{
- "_metadata": {
- "name": "GitHub Repo Vulnerability Scan",
- "description": "Multi-tool vulnerability scanning workflow that scans a GitHub repository for code vulnerabilities using Trivy (repo mode for dependency CVEs) and Semgrep (SAST for code quality and security issues). Evaluates combined findings and produces a unified vulnerability report. Ideal for DevSecOps pipelines and pre-merge security gates.",
- "category": "vulnerability",
- "tags": ["vulnerability", "trivy", "semgrep", "sast", "github", "devsecops"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "GitHub Repo Vulnerability Scan",
- "description": "Scan a GitHub repository for dependency CVEs with Trivy and code-level security issues with Semgrep, evaluate findings, and produce a combined vulnerability report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "vulnerability",
- "tags": ["vulnerability", "trivy", "semgrep", "sast", "github", "devsecops"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "GitHub Repo Vulnerability Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "GitHub Repo Submitted",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "trivy_repo_scan",
- "type": "sentris.trivy.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Scan Repo for Dependency CVEs",
- "config": {
- "params": {
- "scanType": "repo",
- "severity": ["HIGH", "CRITICAL"],
- "format": "json"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "semgrep_sast_scan",
- "type": "sentris.semgrep.run",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "SAST Scan for Code Issues",
- "config": {
- "params": {
- "config": "auto"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_evaluate",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Evaluate Combined Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Generate Vulnerability Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-trivy_repo_scan", "source": "trigger_1", "target": "trivy_repo_scan" },
- {
- "id": "trivy_repo_scan-semgrep_sast_scan",
- "source": "trivy_repo_scan",
- "target": "semgrep_sast_scan"
- },
- {
- "id": "semgrep_sast_scan-script_evaluate",
- "source": "semgrep_sast_scan",
- "target": "script_evaluate"
- },
- {
- "id": "script_evaluate-artifact_report",
- "source": "script_evaluate",
- "target": "artifact_report"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "github_token",
- "type": "string",
- "description": "GitHub Personal Access Token for repository access and scanning"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/github-sast-scan.json b/backend/scripts/seed-templates/github-sast-scan.json
deleted file mode 100644
index eef5b08d..00000000
--- a/backend/scripts/seed-templates/github-sast-scan.json
+++ /dev/null
@@ -1,87 +0,0 @@
-{
- "_metadata": {
- "name": "GitHub SAST Scan",
- "description": "Static Application Security Testing (SAST) workflow that scans a GitHub repository using Semgrep. Detects code-level security issues including injection flaws, hardcoded secrets, insecure patterns, and code quality problems across multiple languages. Evaluates findings by severity and generates a detailed SAST report.",
- "category": "sast",
- "tags": ["sast", "semgrep", "code-analysis", "github", "security", "devsecops"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "GitHub SAST Scan",
- "description": "Perform static application security testing on a GitHub repository using Semgrep, evaluate findings, and produce a SAST report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "sast",
- "tags": ["sast", "semgrep", "code-analysis", "github", "security", "devsecops"],
- "entryPoint": "trigger_1",
- "nodeCount": 4,
- "edgeCount": 3
- },
- "graph": {
- "name": "GitHub SAST Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "GitHub Repo Submitted",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "semgrep_scan",
- "type": "sentris.semgrep.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "SAST Scan with Semgrep",
- "config": {
- "params": {
- "config": "auto"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_evaluate",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Evaluate Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Generate SAST Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-semgrep_scan", "source": "trigger_1", "target": "semgrep_scan" },
- {
- "id": "semgrep_scan-script_evaluate",
- "source": "semgrep_scan",
- "target": "script_evaluate"
- },
- {
- "id": "script_evaluate-artifact_report",
- "source": "script_evaluate",
- "target": "artifact_report"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "github_token",
- "type": "string",
- "description": "GitHub Personal Access Token for repository access and scanning"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/iac-security-scan.json b/backend/scripts/seed-templates/iac-security-scan.json
deleted file mode 100644
index 243fe0b4..00000000
--- a/backend/scripts/seed-templates/iac-security-scan.json
+++ /dev/null
@@ -1,121 +0,0 @@
-{
- "_metadata": {
- "name": "IaC Security Scan",
- "description": "Scan Infrastructure-as-Code files (Terraform, CloudFormation, Kubernetes manifests, Dockerfiles) for security misconfigurations using Checkov. Parses findings by severity, generates an artifact report, and notifies the team via Slack.",
- "category": "compliance",
- "tags": [
- "iac",
- "terraform",
- "cloudformation",
- "kubernetes",
- "checkov",
- "compliance",
- "misconfiguration"
- ],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "IaC Security Scan",
- "description": "Detect security misconfigurations in Terraform, CloudFormation, and K8s manifests with Checkov.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "compliance",
- "tags": [
- "iac",
- "terraform",
- "cloudformation",
- "kubernetes",
- "checkov",
- "compliance",
- "misconfiguration"
- ],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "IaC Security Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": {
- "label": "Start IaC Scan",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "checkov_scan",
- "type": "sentris.checkov.run",
- "position": { "x": 400, "y": 300 },
- "data": {
- "label": "Checkov IaC Scan",
- "config": {
- "params": {
- "framework": "all",
- "softFail": true
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_parse",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 300 },
- "data": {
- "label": "Parse & Categorize Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "semgrep_iac",
- "type": "sentris.semgrep.run",
- "position": { "x": 1000, "y": 300 },
- "data": {
- "label": "Semgrep IaC Pattern Check",
- "config": {
- "params": {
- "config": "p/terraform,p/dockerfile"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 300 },
- "data": {
- "label": "Generate IaC Security Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_team",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 300 },
- "data": {
- "label": "Notify DevSecOps Team",
- "config": { "params": { "channel": "#devsecops" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-checkov_scan", "source": "trigger_1", "target": "checkov_scan" },
- { "id": "checkov_scan-script_parse", "source": "checkov_scan", "target": "script_parse" },
- { "id": "script_parse-semgrep_iac", "source": "script_parse", "target": "semgrep_iac" },
- { "id": "semgrep_iac-artifact_report", "source": "semgrep_iac", "target": "artifact_report" },
- { "id": "artifact_report-notify_team", "source": "artifact_report", "target": "notify_team" }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for IaC scan notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/incident-response-triage.json b/backend/scripts/seed-templates/incident-response-triage.json
deleted file mode 100644
index 35345db3..00000000
--- a/backend/scripts/seed-templates/incident-response-triage.json
+++ /dev/null
@@ -1,100 +0,0 @@
-{
- "_metadata": {
- "name": "Incident Response Triage",
- "description": "Automated incident response workflow that collects alert data, enriches indicators of compromise (IOCs) via VirusTotal and AbuseIPDB, generates a severity assessment, and creates a ticket with findings. Accelerates SOC analyst response time.",
- "category": "incident-response",
- "tags": ["incident-response", "triage", "ioc", "soar", "virustotal"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Incident Response Triage",
- "description": "Enrich IOCs, assess severity, and create tickets automatically on security alerts.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "incident-response",
- "tags": ["incident-response", "triage", "ioc", "soar", "virustotal"],
- "entryPoint": "trigger_1",
- "nodeCount": 7,
- "edgeCount": 7
- },
- "graph": {
- "name": "Incident Response Triage",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": { "label": "Alert Received", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "script_extract",
- "type": "core.logic.script",
- "position": { "x": 350, "y": 300 },
- "data": { "label": "Extract IOCs", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "vt_check",
- "type": "security.virustotal.lookup",
- "position": { "x": 600, "y": 200 },
- "data": { "label": "VirusTotal Lookup", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "abuse_check",
- "type": "security.abuseipdb.check",
- "position": { "x": 600, "y": 400 },
- "data": { "label": "AbuseIPDB Check", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "script_assess",
- "type": "core.logic.script",
- "position": { "x": 900, "y": 300 },
- "data": { "label": "Assess Severity", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "http_ticket",
- "type": "core.http.request",
- "position": { "x": 1150, "y": 300 },
- "data": {
- "label": "Create Incident Ticket",
- "config": { "params": { "method": "POST" }, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_soc",
- "type": "core.notification.slack",
- "position": { "x": 1400, "y": 300 },
- "data": {
- "label": "Notify SOC Team",
- "config": { "params": { "channel": "#soc-incidents" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-script_extract", "source": "trigger_1", "target": "script_extract" },
- { "id": "script_extract-vt_check", "source": "script_extract", "target": "vt_check" },
- { "id": "script_extract-abuse_check", "source": "script_extract", "target": "abuse_check" },
- { "id": "vt_check-script_assess", "source": "vt_check", "target": "script_assess" },
- { "id": "abuse_check-script_assess", "source": "abuse_check", "target": "script_assess" },
- { "id": "script_assess-http_ticket", "source": "script_assess", "target": "http_ticket" },
- { "id": "http_ticket-notify_soc", "source": "http_ticket", "target": "notify_soc" }
- ]
- },
- "requiredSecrets": [
- {
- "name": "virustotal_api_key",
- "type": "string",
- "description": "VirusTotal API key for threat intelligence lookups"
- },
- {
- "name": "abuseipdb_api_key",
- "type": "string",
- "description": "AbuseIPDB API key for IP reputation checks"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for SOC notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/ioc-enrichment-workflow.json b/backend/scripts/seed-templates/ioc-enrichment-workflow.json
deleted file mode 100644
index 3dca6624..00000000
--- a/backend/scripts/seed-templates/ioc-enrichment-workflow.json
+++ /dev/null
@@ -1,142 +0,0 @@
-{
- "_metadata": {
- "name": "IOC Enrichment Workflow",
- "description": "Automated indicator of compromise (IOC) enrichment pipeline that ingests raw IOCs (IPs, domains, hashes), queries multiple threat intelligence sources in parallel — VirusTotal, AbuseIPDB, and abuse.ch — correlates findings, assigns a composite threat score, and produces an enrichment report artifact. Use for threat hunting and SOC triage acceleration.",
- "category": "threat-intelligence",
- "tags": ["ioc", "threat-intel", "enrichment", "virustotal", "abuseipdb", "threat-hunting"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "IOC Enrichment Workflow",
- "description": "Enrich IOCs from multiple threat intel sources, score severity, and produce an enrichment report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "threat-intelligence",
- "tags": ["ioc", "threat-intel", "enrichment", "virustotal", "abuseipdb", "threat-hunting"],
- "entryPoint": "trigger_1",
- "nodeCount": 8,
- "edgeCount": 9
- },
- "graph": {
- "name": "IOC Enrichment Workflow",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": { "label": "IOCs Submitted", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "script_parse",
- "type": "core.logic.script",
- "position": { "x": 400, "y": 300 },
- "data": {
- "label": "Parse & Classify IOCs",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "vt_enrich",
- "type": "security.virustotal.lookup",
- "position": { "x": 700, "y": 150 },
- "data": {
- "label": "VirusTotal Enrichment",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "abuse_enrich",
- "type": "security.abuseipdb.check",
- "position": { "x": 700, "y": 300 },
- "data": {
- "label": "AbuseIPDB Enrichment",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "http_abusech",
- "type": "core.http.request",
- "position": { "x": 700, "y": 450 },
- "data": {
- "label": "abuse.ch Threat Feed",
- "config": {
- "params": { "method": "POST", "url": "https://urlhaus-api.abuse.ch/v1/" },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_correlate",
- "type": "core.logic.script",
- "position": { "x": 1050, "y": 300 },
- "data": {
- "label": "Correlate & Score Threats",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1350, "y": 300 },
- "data": {
- "label": "Write Enrichment Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_analyst",
- "type": "core.notification.slack",
- "position": { "x": 1650, "y": 300 },
- "data": {
- "label": "Alert Analyst",
- "config": { "params": { "channel": "#threat-intel" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-script_parse", "source": "trigger_1", "target": "script_parse" },
- { "id": "script_parse-vt_enrich", "source": "script_parse", "target": "vt_enrich" },
- { "id": "script_parse-abuse_enrich", "source": "script_parse", "target": "abuse_enrich" },
- { "id": "script_parse-http_abusech", "source": "script_parse", "target": "http_abusech" },
- { "id": "vt_enrich-script_correlate", "source": "vt_enrich", "target": "script_correlate" },
- {
- "id": "abuse_enrich-script_correlate",
- "source": "abuse_enrich",
- "target": "script_correlate"
- },
- {
- "id": "http_abusech-script_correlate",
- "source": "http_abusech",
- "target": "script_correlate"
- },
- {
- "id": "script_correlate-artifact_report",
- "source": "script_correlate",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_analyst",
- "source": "artifact_report",
- "target": "notify_analyst"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "virustotal_api_key",
- "type": "string",
- "description": "VirusTotal API key for hash, domain, and IP lookups"
- },
- {
- "name": "abuseipdb_api_key",
- "type": "string",
- "description": "AbuseIPDB API key for IP reputation checks"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for threat intel notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/kubernetes-security-audit.json b/backend/scripts/seed-templates/kubernetes-security-audit.json
deleted file mode 100644
index 5f6a17ef..00000000
--- a/backend/scripts/seed-templates/kubernetes-security-audit.json
+++ /dev/null
@@ -1,159 +0,0 @@
-{
- "_metadata": {
- "name": "Kubernetes Security Audit",
- "description": "Automated Kubernetes cluster security assessment that queries the K8s API to collect RBAC policies, pod security configurations, and network policies. Analyzes findings for overly permissive roles, containers running as root, missing security contexts, and absent network segmentation. Produces a prioritized audit report with remediation guidance and notifies the team via Slack.",
- "category": "kubernetes-security",
- "tags": ["kubernetes", "k8s", "rbac", "pod-security", "network-policy", "cluster-audit"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Kubernetes Security Audit",
- "description": "Audit Kubernetes RBAC, pod security, and network policies for misconfigurations and generate a remediation report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "kubernetes-security",
- "tags": ["kubernetes", "k8s", "rbac", "pod-security", "network-policy", "cluster-audit"],
- "entryPoint": "trigger_1",
- "nodeCount": 8,
- "edgeCount": 9
- },
- "graph": {
- "name": "Kubernetes Security Audit",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": {
- "label": "Start K8s Security Audit",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "http_rbac",
- "type": "core.http.request",
- "position": { "x": 400, "y": 150 },
- "data": {
- "label": "Fetch RBAC Policies",
- "config": {
- "params": { "method": "GET", "path": "/apis/rbac.authorization.k8s.io/v1" },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "http_pods",
- "type": "core.http.request",
- "position": { "x": 400, "y": 300 },
- "data": {
- "label": "Fetch Pod Security Configs",
- "config": {
- "params": { "method": "GET", "path": "/api/v1/pods" },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "http_netpol",
- "type": "core.http.request",
- "position": { "x": 400, "y": 450 },
- "data": {
- "label": "Fetch Network Policies",
- "config": {
- "params": { "method": "GET", "path": "/apis/networking.k8s.io/v1/networkpolicies" },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_analyze_rbac",
- "type": "core.logic.script",
- "position": { "x": 750, "y": 300 },
- "data": {
- "label": "Analyze Security Posture",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_remediation",
- "type": "core.logic.script",
- "position": { "x": 1050, "y": 300 },
- "data": {
- "label": "Generate Remediation Guidance",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1350, "y": 300 },
- "data": {
- "label": "Save K8s Audit Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1650, "y": 300 },
- "data": {
- "label": "Notify Platform Team",
- "config": { "params": { "channel": "#k8s-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-http_rbac", "source": "trigger_1", "target": "http_rbac" },
- { "id": "trigger_1-http_pods", "source": "trigger_1", "target": "http_pods" },
- { "id": "trigger_1-http_netpol", "source": "trigger_1", "target": "http_netpol" },
- {
- "id": "http_rbac-script_analyze_rbac",
- "source": "http_rbac",
- "target": "script_analyze_rbac"
- },
- {
- "id": "http_pods-script_analyze_rbac",
- "source": "http_pods",
- "target": "script_analyze_rbac"
- },
- {
- "id": "http_netpol-script_analyze_rbac",
- "source": "http_netpol",
- "target": "script_analyze_rbac"
- },
- {
- "id": "script_analyze_rbac-script_remediation",
- "source": "script_analyze_rbac",
- "target": "script_remediation"
- },
- {
- "id": "script_remediation-artifact_report",
- "source": "script_remediation",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "k8s_api_token",
- "type": "string",
- "description": "Kubernetes API bearer token with read access to RBAC, pods, and network policies"
- },
- {
- "name": "k8s_api_url",
- "type": "string",
- "description": "Kubernetes API server URL (e.g., https://cluster.example.com:6443)"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for Kubernetes audit notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/log-analysis-pipeline.json b/backend/scripts/seed-templates/log-analysis-pipeline.json
deleted file mode 100644
index de37639b..00000000
--- a/backend/scripts/seed-templates/log-analysis-pipeline.json
+++ /dev/null
@@ -1,124 +0,0 @@
-{
- "_metadata": {
- "name": "Log Analysis Pipeline",
- "description": "Security log analysis pipeline that ingests logs via HTTP, parses structured and unstructured formats, extracts indicators of compromise (IPs, domains, hashes, URLs), enriches extracted IOCs against VirusTotal, correlates findings across log sources, and produces a threat intelligence report. Designed for SOC teams processing SIEM exports, firewall logs, or application audit trails.",
- "category": "log-analysis",
- "tags": ["logs", "siem", "ioc-extraction", "correlation", "threat-intel", "virustotal"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Log Analysis Pipeline",
- "description": "Ingest security logs, extract and enrich IOCs, correlate findings, and generate a threat intelligence report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "log-analysis",
- "tags": ["logs", "siem", "ioc-extraction", "correlation", "threat-intel", "virustotal"],
- "entryPoint": "trigger_1",
- "nodeCount": 7,
- "edgeCount": 6
- },
- "graph": {
- "name": "Log Analysis Pipeline",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start Log Analysis",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "http_collect",
- "type": "core.http.request",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Collect Logs from Source",
- "config": {
- "params": { "method": "GET" },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_parse",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Parse & Extract IOCs",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "vt_enrich",
- "type": "security.virustotal.lookup",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Enrich IOCs via VirusTotal",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_correlate",
- "type": "core.logic.script",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Correlate & Score Threats",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Generate Threat Intel Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1900, "y": 250 },
- "data": {
- "label": "Notify SOC Team",
- "config": { "params": { "channel": "#soc-alerts" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-http_collect", "source": "trigger_1", "target": "http_collect" },
- { "id": "http_collect-script_parse", "source": "http_collect", "target": "script_parse" },
- { "id": "script_parse-vt_enrich", "source": "script_parse", "target": "vt_enrich" },
- {
- "id": "vt_enrich-script_correlate",
- "source": "vt_enrich",
- "target": "script_correlate"
- },
- {
- "id": "script_correlate-artifact_report",
- "source": "script_correlate",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "virustotal_api_key",
- "type": "string",
- "description": "VirusTotal API key for IOC enrichment lookups"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for SOC threat intelligence alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/malware-indicator-scan.json b/backend/scripts/seed-templates/malware-indicator-scan.json
deleted file mode 100644
index 30c1d8b6..00000000
--- a/backend/scripts/seed-templates/malware-indicator-scan.json
+++ /dev/null
@@ -1,122 +0,0 @@
-{
- "_metadata": {
- "name": "Malware Indicator Scan",
- "description": "Combined malware analysis workflow using VirusTotal threat intelligence and YARA pattern matching. Checks indicators of compromise against VirusTotal's database and scans file content with YARA rules in parallel, then correlates findings into a unified threat report delivered via Slack. Ideal for incident response and threat hunting.",
- "category": "malware-analysis",
- "tags": ["malware", "virustotal", "yara", "threat-intelligence", "ioc"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Malware Indicator Scan",
- "description": "Analyze indicators with VirusTotal and scan files with YARA rules, then correlate and report findings.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "malware-analysis",
- "tags": ["malware", "virustotal", "yara", "threat-intelligence", "ioc"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 6
- },
- "graph": {
- "name": "Malware Indicator Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Start Malware Analysis",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "virustotal_1",
- "type": "security.virustotal.lookup",
- "position": { "x": 450, "y": 100 },
- "data": {
- "label": "VirusTotal Lookup",
- "config": {
- "params": {
- "type": "file"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "yara_1",
- "type": "sentris.yara.run",
- "position": { "x": 450, "y": 400 },
- "data": {
- "label": "YARA Rule Scan",
- "config": {
- "params": {
- "timeout": 60
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "correlate_findings",
- "type": "core.logic.script",
- "position": { "x": 800, "y": 250 },
- "data": {
- "label": "Correlate Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1100, "y": 250 },
- "data": {
- "label": "Save Threat Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1400, "y": 250 },
- "data": {
- "label": "Alert Security Team",
- "config": { "params": { "channel": "#threat-intel" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-virustotal_1", "source": "trigger_1", "target": "virustotal_1" },
- { "id": "trigger_1-yara_1", "source": "trigger_1", "target": "yara_1" },
- {
- "id": "virustotal_1-correlate_findings",
- "source": "virustotal_1",
- "target": "correlate_findings"
- },
- { "id": "yara_1-correlate_findings", "source": "yara_1", "target": "correlate_findings" },
- {
- "id": "correlate_findings-artifact_report",
- "source": "correlate_findings",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "virustotal_api_key",
- "type": "string",
- "description": "VirusTotal API key for threat intelligence lookups"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for malware analysis alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/network-recon-pipeline.json b/backend/scripts/seed-templates/network-recon-pipeline.json
deleted file mode 100644
index 89d3f20e..00000000
--- a/backend/scripts/seed-templates/network-recon-pipeline.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "_metadata": {
- "name": "Network Reconnaissance Pipeline",
- "description": "Comprehensive network reconnaissance workflow that discovers subdomains via Subfinder, resolves DNS records with DNSX, scans open ports using Naabu, and probes live HTTP services with HTTPX. Results are aggregated into a unified recon report and delivered via Slack. Ideal for mapping external attack surface before penetration testing.",
- "category": "network-security",
- "tags": ["recon", "subdomain", "dns", "port-scan", "network", "attack-surface"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Network Reconnaissance Pipeline",
- "description": "Discover subdomains, resolve DNS, scan ports, and probe HTTP services for full attack surface mapping.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "network-security",
- "tags": ["recon", "subdomain", "dns", "port-scan", "network", "attack-surface"],
- "entryPoint": "trigger_1",
- "nodeCount": 7,
- "edgeCount": 7
- },
- "graph": {
- "name": "Network Reconnaissance Pipeline",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start Recon", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "subfinder_1",
- "type": "sentris.subfinder.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Enumerate Subdomains",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "dnsx_1",
- "type": "sentris.dnsx.run",
- "position": { "x": 700, "y": 150 },
- "data": {
- "label": "Resolve DNS Records",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "naabu_1",
- "type": "sentris.naabu.scan",
- "position": { "x": 700, "y": 350 },
- "data": {
- "label": "Scan Open Ports",
- "config": { "params": { "topPorts": "1000" }, "inputOverrides": {} }
- }
- },
- {
- "id": "httpx_1",
- "type": "sentris.httpx.scan",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Probe HTTP Services",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_report",
- "type": "core.logic.script",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Aggregate Recon Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Send Recon Report",
- "config": { "params": { "channel": "#recon-results" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-subfinder_1", "source": "trigger_1", "target": "subfinder_1" },
- { "id": "subfinder_1-dnsx_1", "source": "subfinder_1", "target": "dnsx_1" },
- { "id": "subfinder_1-naabu_1", "source": "subfinder_1", "target": "naabu_1" },
- { "id": "dnsx_1-httpx_1", "source": "dnsx_1", "target": "httpx_1" },
- { "id": "naabu_1-httpx_1", "source": "naabu_1", "target": "httpx_1" },
- { "id": "httpx_1-script_report", "source": "httpx_1", "target": "script_report" },
- {
- "id": "script_report-notify_results",
- "source": "script_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for recon results channel"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/npm-dependency-cve-hunt.json b/backend/scripts/seed-templates/npm-dependency-cve-hunt.json
new file mode 100644
index 00000000..21d7785c
--- /dev/null
+++ b/backend/scripts/seed-templates/npm-dependency-cve-hunt.json
@@ -0,0 +1,159 @@
+{
+ "_metadata": {
+ "name": "NPM Dependency CVE Hunt",
+ "description": "Supply-chain research workflow that checks npm package/version specs against OSV, ranks CVEs and malicious-package advisories, and writes an actionable upgrade report.",
+ "category": "dependency-security",
+ "tags": ["npm", "dependency", "cve", "osv", "supply-chain", "malicious-packages"],
+ "author": "sentris-team",
+ "version": "1.0.0"
+ },
+ "manifest": {
+ "name": "NPM Dependency CVE Hunt",
+ "description": "Check npm package/version specs against OSV and produce a prioritized dependency CVE report.",
+ "version": "1.0.0",
+ "author": "sentris-team",
+ "category": "dependency-security",
+ "tags": ["npm", "dependency", "cve", "osv", "supply-chain", "malicious-packages"],
+ "entryPoint": "trigger_1",
+ "nodeCount": 4,
+ "edgeCount": 6
+ },
+ "graph": {
+ "name": "NPM Dependency CVE Hunt",
+ "description": "Package CVE and malicious package triage workflow for npm dependency research.",
+ "nodes": [
+ {
+ "id": "trigger_1",
+ "type": "core.workflow.entrypoint",
+ "position": { "x": 100, "y": 260 },
+ "data": {
+ "label": "NPM Packages Input",
+ "config": {
+ "params": {
+ "runtimeInputs": [
+ {
+ "id": "packageSpecs",
+ "label": "NPM package specs",
+ "type": "array",
+ "required": true,
+ "description": "Package names with optional versions, for example lodash@4.17.20, @scope/pkg@1.2.3, or axios."
+ },
+ {
+ "id": "researchNotes",
+ "label": "Research notes",
+ "type": "text",
+ "required": false,
+ "description": "Optional program, repository, or triage context to include in the report."
+ }
+ ]
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "osv_query",
+ "type": "sentris.osv.query",
+ "position": { "x": 420, "y": 220 },
+ "data": {
+ "label": "Query OSV Dependency Advisories",
+ "config": {
+ "params": {
+ "ecosystem": "npm",
+ "severityFloor": "medium",
+ "hydrateAdvisories": true,
+ "maxAdvisoriesPerPackage": 50,
+ "includeUnknownSeverity": true
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "assemble_dependency_report",
+ "type": "core.logic.script",
+ "position": { "x": 740, "y": 260 },
+ "data": {
+ "label": "Assemble Dependency CVE Report",
+ "config": {
+ "params": {
+ "variables": [
+ { "name": "findings", "type": "list-json" },
+ { "name": "summary", "type": "json" },
+ { "name": "packages", "type": "list-json" },
+ { "name": "researchNotes", "type": "string" }
+ ],
+ "returns": [{ "name": "report", "type": "json" }],
+ "code": "export function script(input) {\n const findings = Array.isArray(input.findings) ? input.findings : [];\n const packages = Array.isArray(input.packages) ? input.packages : [];\n const summary = input.summary && typeof input.summary === 'object' ? input.summary : { packagesChecked: packages.length, vulnerablePackages: 0, findings: findings.length, maliciousPackageRecords: 0, countsBySeverity: {} };\n return { report: { summary, researchNotes: input.researchNotes || null, packages, priorityFindings: findings.slice(0, 100), nextSteps: ['Prioritize malicious package records and critical/high CVEs first', 'Confirm the vulnerable package is reachable in the application dependency graph', 'Upgrade to the fixed version when listed, or evaluate the advisory workaround', 'Re-run this workflow after dependency updates to verify closure'] } };\n}"
+ },
+ "inputOverrides": {}
+ }
+ }
+ },
+ {
+ "id": "artifact_report",
+ "type": "core.artifact.writer",
+ "position": { "x": 1060, "y": 260 },
+ "data": {
+ "label": "Save Dependency CVE Report",
+ "config": {
+ "params": {
+ "fileExtension": ".json",
+ "mimeType": "application/json",
+ "saveToRunArtifacts": true,
+ "publishToArtifactLibrary": false
+ },
+ "inputOverrides": {
+ "artifactName": "npm-dependency-cve-hunt-{{date}}"
+ }
+ }
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "trigger_1-osv_query-packageSpecs",
+ "source": "trigger_1",
+ "target": "osv_query",
+ "sourceHandle": "packageSpecs",
+ "targetHandle": "packageSpecs"
+ },
+ {
+ "id": "osv_query-assemble_dependency_report-findings",
+ "source": "osv_query",
+ "target": "assemble_dependency_report",
+ "sourceHandle": "findings",
+ "targetHandle": "findings"
+ },
+ {
+ "id": "osv_query-assemble_dependency_report-summary",
+ "source": "osv_query",
+ "target": "assemble_dependency_report",
+ "sourceHandle": "summary",
+ "targetHandle": "summary"
+ },
+ {
+ "id": "osv_query-assemble_dependency_report-packages",
+ "source": "osv_query",
+ "target": "assemble_dependency_report",
+ "sourceHandle": "packages",
+ "targetHandle": "packages"
+ },
+ {
+ "id": "trigger_1-assemble_dependency_report-researchNotes",
+ "source": "trigger_1",
+ "target": "assemble_dependency_report",
+ "sourceHandle": "researchNotes",
+ "targetHandle": "researchNotes"
+ },
+ {
+ "id": "assemble_dependency_report-artifact_report-report",
+ "source": "assemble_dependency_report",
+ "target": "artifact_report",
+ "sourceHandle": "report",
+ "targetHandle": "content"
+ }
+ ]
+ },
+ "requiredSecrets": []
+}
diff --git a/backend/scripts/seed-templates/npm-dependency-scan.json b/backend/scripts/seed-templates/npm-dependency-scan.json
deleted file mode 100644
index 6ab88cf2..00000000
--- a/backend/scripts/seed-templates/npm-dependency-scan.json
+++ /dev/null
@@ -1,83 +0,0 @@
-{
- "_metadata": {
- "name": "NPM Dependency Scan",
- "description": "Automated dependency vulnerability scanning workflow for npm/Node.js projects using Trivy filesystem scanning. Analyzes package-lock.json, yarn.lock, and node_modules to detect known CVEs in third-party dependencies. Evaluates findings by severity and generates an artifact report for supply chain security audits.",
- "category": "dependency-scanning",
- "tags": ["npm", "nodejs", "dependencies", "trivy", "vulnerability", "supply-chain"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "NPM Dependency Scan",
- "description": "Scan npm/Node.js project dependencies for known CVEs using Trivy filesystem scanning, evaluate findings, and produce a dependency vulnerability report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "dependency-scanning",
- "tags": ["npm", "nodejs", "dependencies", "trivy", "vulnerability", "supply-chain"],
- "entryPoint": "trigger_1",
- "nodeCount": 4,
- "edgeCount": 3
- },
- "graph": {
- "name": "NPM Dependency Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Project Path Submitted",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "trivy_fs_scan",
- "type": "sentris.trivy.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Scan NPM Dependencies for CVEs",
- "config": {
- "params": {
- "scanType": "fs",
- "severity": ["HIGH", "CRITICAL"],
- "format": "json"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_evaluate",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Evaluate Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Generate Dependency Scan Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-trivy_fs_scan", "source": "trigger_1", "target": "trivy_fs_scan" },
- {
- "id": "trivy_fs_scan-script_evaluate",
- "source": "trivy_fs_scan",
- "target": "script_evaluate"
- },
- {
- "id": "script_evaluate-artifact_report",
- "source": "script_evaluate",
- "target": "artifact_report"
- }
- ]
- },
- "requiredSecrets": []
-}
diff --git a/backend/scripts/seed-templates/osint-reconnaissance.json b/backend/scripts/seed-templates/osint-reconnaissance.json
deleted file mode 100644
index cfa7b393..00000000
--- a/backend/scripts/seed-templates/osint-reconnaissance.json
+++ /dev/null
@@ -1,99 +0,0 @@
-{
- "_metadata": {
- "name": "OSINT Reconnaissance",
- "description": "Automated OSINT reconnaissance workflow using theHarvester. Gathers emails, subdomains, and IP addresses from public data sources for a target domain. Results are compiled into a structured intelligence report and delivered via Slack. Ideal for pre-engagement reconnaissance and attack surface mapping.",
- "category": "reconnaissance",
- "tags": ["osint", "reconnaissance", "theharvester", "emails", "subdomains"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "OSINT Reconnaissance",
- "description": "Gather emails, subdomains, and IPs from public sources, then deliver an intelligence report via Slack.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "reconnaissance",
- "tags": ["osint", "reconnaissance", "theharvester", "emails", "subdomains"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "OSINT Reconnaissance",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start OSINT Recon", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "theharvester_1",
- "type": "sentris.theharvester.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Harvest OSINT Data",
- "config": {
- "params": {
- "sources": "baidu,bing,duckduckgo,yahoo",
- "limit": 500
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "format_report",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Format Intelligence Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Save OSINT Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#threat-intel" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-theharvester_1", "source": "trigger_1", "target": "theharvester_1" },
- {
- "id": "theharvester_1-format_report",
- "source": "theharvester_1",
- "target": "format_report"
- },
- {
- "id": "format_report-artifact_report",
- "source": "format_report",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for OSINT reconnaissance alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/phishing-email-analysis.json b/backend/scripts/seed-templates/phishing-email-analysis.json
deleted file mode 100644
index 56d66a6d..00000000
--- a/backend/scripts/seed-templates/phishing-email-analysis.json
+++ /dev/null
@@ -1,92 +0,0 @@
-{
- "_metadata": {
- "name": "Phishing Email Analysis",
- "description": "Analyze suspicious emails by extracting URLs and attachments, checking them against VirusTotal for malware and phishing indicators, assessing risk, and delivering a verdict. Designed for SOC teams handling phishing reports.",
- "category": "security",
- "tags": ["phishing", "email", "analysis", "virustotal", "soar"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Phishing Email Analysis",
- "description": "Extract, scan, and classify suspicious emails with automated threat intelligence.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "security",
- "tags": ["phishing", "email", "analysis", "virustotal", "soar"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 6
- },
- "graph": {
- "name": "Phishing Email Analysis",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Email Submitted", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "script_extract",
- "type": "core.logic.script",
- "position": { "x": 350, "y": 250 },
- "data": { "label": "Extract Indicators", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "vt_urls",
- "type": "security.virustotal.lookup",
- "position": { "x": 600, "y": 150 },
- "data": { "label": "Scan URLs", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "http_domain",
- "type": "core.http.request",
- "position": { "x": 600, "y": 350 },
- "data": {
- "label": "Check Sender Domain",
- "config": { "params": { "method": "GET" }, "inputOverrides": {} }
- }
- },
- {
- "id": "script_verdict",
- "type": "core.logic.script",
- "position": { "x": 900, "y": 250 },
- "data": { "label": "Generate Verdict", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "notify_verdict",
- "type": "core.notification.slack",
- "position": { "x": 1200, "y": 250 },
- "data": {
- "label": "Report Verdict",
- "config": { "params": { "channel": "#phishing-reports" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-script_extract", "source": "trigger_1", "target": "script_extract" },
- { "id": "script_extract-vt_urls", "source": "script_extract", "target": "vt_urls" },
- { "id": "script_extract-http_domain", "source": "script_extract", "target": "http_domain" },
- { "id": "vt_urls-script_verdict", "source": "vt_urls", "target": "script_verdict" },
- { "id": "http_domain-script_verdict", "source": "http_domain", "target": "script_verdict" },
- {
- "id": "script_verdict-notify_verdict",
- "source": "script_verdict",
- "target": "notify_verdict"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "virustotal_api_key",
- "type": "string",
- "description": "VirusTotal API key for URL and file scanning"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack webhook URL for phishing analysis results"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/pip-dependency-scan.json b/backend/scripts/seed-templates/pip-dependency-scan.json
deleted file mode 100644
index 16b266e9..00000000
--- a/backend/scripts/seed-templates/pip-dependency-scan.json
+++ /dev/null
@@ -1,83 +0,0 @@
-{
- "_metadata": {
- "name": "Pip Dependency Scan",
- "description": "Automated dependency vulnerability scanning workflow for Python/pip projects using Trivy filesystem scanning. Analyzes requirements.txt, Pipfile.lock, and poetry.lock to detect known CVEs in third-party dependencies. Evaluates findings by severity and generates an artifact report for supply chain security audits.",
- "category": "dependency-scanning",
- "tags": ["python", "pip", "dependencies", "trivy", "vulnerability", "supply-chain"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Pip Dependency Scan",
- "description": "Scan Python/pip project dependencies for known CVEs using Trivy filesystem scanning, evaluate findings, and produce a dependency vulnerability report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "dependency-scanning",
- "tags": ["python", "pip", "dependencies", "trivy", "vulnerability", "supply-chain"],
- "entryPoint": "trigger_1",
- "nodeCount": 4,
- "edgeCount": 3
- },
- "graph": {
- "name": "Pip Dependency Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": {
- "label": "Project Path Submitted",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "trivy_fs_scan",
- "type": "sentris.trivy.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Scan Python Dependencies for CVEs",
- "config": {
- "params": {
- "scanType": "fs",
- "severity": ["HIGH", "CRITICAL"],
- "format": "json"
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "script_evaluate",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Evaluate Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Generate Dependency Scan Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-trivy_fs_scan", "source": "trigger_1", "target": "trivy_fs_scan" },
- {
- "id": "trivy_fs_scan-script_evaluate",
- "source": "trivy_fs_scan",
- "target": "script_evaluate"
- },
- {
- "id": "script_evaluate-artifact_report",
- "source": "script_evaluate",
- "target": "artifact_report"
- }
- ]
- },
- "requiredSecrets": []
-}
diff --git a/backend/scripts/seed-templates/port-scanning.json b/backend/scripts/seed-templates/port-scanning.json
deleted file mode 100644
index 6828ba2e..00000000
--- a/backend/scripts/seed-templates/port-scanning.json
+++ /dev/null
@@ -1,94 +0,0 @@
-{
- "_metadata": {
- "name": "Port Scanning",
- "description": "Scan targets for open ports using Naabu with the top 1000 most common ports. Results are formatted into a structured report and delivered via Slack. Use this template to quickly identify exposed services across your infrastructure before deeper vulnerability assessment.",
- "category": "network-security",
- "tags": ["port-scan", "network", "naabu", "recon"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Port Scanning",
- "description": "Scan for open ports with Naabu, format a port report, and notify via Slack.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "network-security",
- "tags": ["port-scan", "network", "naabu", "recon"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "Port Scanning",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start Port Scan", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "naabu_1",
- "type": "sentris.naabu.scan",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Scan Open Ports",
- "config": {
- "params": {
- "topPorts": 1000
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "format_results",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Format Port Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Save Port Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#security-ops" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-naabu_1", "source": "trigger_1", "target": "naabu_1" },
- { "id": "naabu_1-format_results", "source": "naabu_1", "target": "format_results" },
- {
- "id": "format_results-artifact_report",
- "source": "format_results",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for port scanning alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/ssl-tls-audit.json b/backend/scripts/seed-templates/ssl-tls-audit.json
deleted file mode 100644
index 14f8cba3..00000000
--- a/backend/scripts/seed-templates/ssl-tls-audit.json
+++ /dev/null
@@ -1,85 +0,0 @@
-{
- "_metadata": {
- "name": "SSL/TLS Audit",
- "description": "Automated SSL/TLS security audit workflow that first verifies target reachability with HTTPX, then performs a comprehensive TLS configuration assessment using testssl.sh. Checks certificate validity, cipher suites, protocol versions, and known vulnerabilities (BEAST, POODLE, Heartbleed, etc.). Results are compiled into an artifact report and delivered via Slack.",
- "category": "network-security",
- "tags": ["ssl", "tls", "certificate", "audit", "testssl"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "SSL/TLS Audit",
- "description": "Verify target reachability, audit TLS configuration and certificates, and deliver a compliance report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "network-security",
- "tags": ["ssl", "tls", "certificate", "audit", "testssl"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "SSL/TLS Audit",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start TLS Audit", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "httpx_1",
- "type": "sentris.httpx.scan",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Verify Target Reachability",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "testssl_1",
- "type": "sentris.testssl.run",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Audit TLS Configuration",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Generate Audit Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#tls-audit" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-httpx_1", "source": "trigger_1", "target": "httpx_1" },
- { "id": "httpx_1-testssl_1", "source": "httpx_1", "target": "testssl_1" },
- { "id": "testssl_1-artifact_report", "source": "testssl_1", "target": "artifact_report" },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for TLS audit notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/subdomain-enumeration.json b/backend/scripts/seed-templates/subdomain-enumeration.json
deleted file mode 100644
index 4c80234d..00000000
--- a/backend/scripts/seed-templates/subdomain-enumeration.json
+++ /dev/null
@@ -1,111 +0,0 @@
-{
- "_metadata": {
- "name": "Subdomain Enumeration",
- "description": "Discover subdomains using Amass passive enumeration, then resolve DNS records with DNSX to validate findings. Results are formatted into a structured report and delivered via Slack. Ideal for mapping an organization's external attack surface before deeper testing.",
- "category": "network-security",
- "tags": ["subdomain", "dns", "recon", "amass", "dnsx"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Subdomain Enumeration",
- "description": "Enumerate subdomains with Amass and resolve DNS records with DNSX, then report findings via Slack.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "network-security",
- "tags": ["subdomain", "dns", "recon", "amass", "dnsx"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "Subdomain Enumeration",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start Enumeration", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "amass_1",
- "type": "sentris.amass.enum",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Enumerate Subdomains",
- "config": {
- "params": {
- "passive": true,
- "active": false,
- "bruteForce": false
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "dnsx_1",
- "type": "sentris.dnsx.run",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Resolve DNS Records",
- "config": {
- "params": {
- "recordTypes": ["A", "AAAA", "CNAME"]
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "format_results",
- "type": "core.logic.script",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Format Enumeration Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Save Subdomain Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#recon-results" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-amass_1", "source": "trigger_1", "target": "amass_1" },
- { "id": "amass_1-dnsx_1", "source": "amass_1", "target": "dnsx_1" },
- { "id": "dnsx_1-format_results", "source": "dnsx_1", "target": "format_results" },
- {
- "id": "format_results-artifact_report",
- "source": "format_results",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for subdomain enumeration results"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/supabase-security-audit.json b/backend/scripts/seed-templates/supabase-security-audit.json
deleted file mode 100644
index dd447499..00000000
--- a/backend/scripts/seed-templates/supabase-security-audit.json
+++ /dev/null
@@ -1,98 +0,0 @@
-{
- "_metadata": {
- "name": "Supabase Security Audit",
- "description": "Scan Supabase projects for Row Level Security (RLS) misconfigurations, exposed API keys, open storage buckets, and insecure database settings. Findings are parsed, compiled into an artifact report, and delivered to the security team via Slack.",
- "category": "cloud-security",
- "tags": ["supabase", "rls", "cloud-security", "database", "storage", "api-keys", "audit"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Supabase Security Audit",
- "description": "Detect RLS misconfigurations, exposed keys, and open storage buckets in Supabase projects.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "cloud-security",
- "tags": ["supabase", "rls", "cloud-security", "database", "storage", "api-keys", "audit"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "Supabase Security Audit",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 300 },
- "data": {
- "label": "Start Supabase Audit",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "supabase_scan",
- "type": "sentris.supabase.scanner",
- "position": { "x": 450, "y": 300 },
- "data": {
- "label": "Supabase Security Scan",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "script_parse",
- "type": "core.logic.script",
- "position": { "x": 800, "y": 300 },
- "data": {
- "label": "Parse & Classify Findings",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1150, "y": 300 },
- "data": {
- "label": "Generate Supabase Audit Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_team",
- "type": "core.notification.slack",
- "position": { "x": 1500, "y": 300 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#cloud-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-supabase_scan", "source": "trigger_1", "target": "supabase_scan" },
- { "id": "supabase_scan-script_parse", "source": "supabase_scan", "target": "script_parse" },
- {
- "id": "script_parse-artifact_report",
- "source": "script_parse",
- "target": "artifact_report"
- },
- { "id": "artifact_report-notify_team", "source": "artifact_report", "target": "notify_team" }
- ]
- },
- "requiredSecrets": [
- {
- "name": "supabase_url",
- "type": "string",
- "description": "Supabase project URL (e.g., https://your-project.supabase.co)"
- },
- {
- "name": "supabase_service_role_key",
- "type": "string",
- "description": "Supabase service role key with admin access for security scanning"
- },
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for Supabase audit notifications"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/vulnerability-scanning-pipeline.json b/backend/scripts/seed-templates/vulnerability-scanning-pipeline.json
deleted file mode 100644
index 253b5f52..00000000
--- a/backend/scripts/seed-templates/vulnerability-scanning-pipeline.json
+++ /dev/null
@@ -1,82 +0,0 @@
-{
- "_metadata": {
- "name": "Vulnerability Scanning Pipeline",
- "description": "Multi-stage vulnerability scanning workflow that discovers subdomains, probes for HTTP services, runs Nuclei vulnerability scans, and sends results via Slack. Ideal for continuous attack surface monitoring.",
- "category": "security",
- "tags": ["vulnerability", "scanning", "nuclei", "recon", "automation"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Vulnerability Scanning Pipeline",
- "description": "Discover subdomains, probe HTTP services, scan for vulnerabilities, and notify results.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "security",
- "tags": ["vulnerability", "scanning", "nuclei", "recon", "automation"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "Vulnerability Scanning Pipeline",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 200 },
- "data": { "label": "Start Scan", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "subfinder_1",
- "type": "sentris.subfinder.run",
- "position": { "x": 350, "y": 200 },
- "data": { "label": "Discover Subdomains", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "httpx_1",
- "type": "sentris.httpx.scan",
- "position": { "x": 600, "y": 200 },
- "data": { "label": "Probe HTTP Services", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "nuclei_1",
- "type": "sentris.nuclei.scan",
- "position": { "x": 850, "y": 200 },
- "data": {
- "label": "Run Nuclei Scan",
- "config": { "params": { "severity": "medium,high,critical" }, "inputOverrides": {} }
- }
- },
- {
- "id": "script_1",
- "type": "core.logic.script",
- "position": { "x": 1100, "y": 200 },
- "data": { "label": "Format Report", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "notify_1",
- "type": "core.notification.slack",
- "position": { "x": 1350, "y": 200 },
- "data": {
- "label": "Send Slack Alert",
- "config": { "params": { "channel": "#security-alerts" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-subfinder_1", "source": "trigger_1", "target": "subfinder_1" },
- { "id": "subfinder_1-httpx_1", "source": "subfinder_1", "target": "httpx_1" },
- { "id": "httpx_1-nuclei_1", "source": "httpx_1", "target": "nuclei_1" },
- { "id": "nuclei_1-script_1", "source": "nuclei_1", "target": "script_1" },
- { "id": "script_1-notify_1", "source": "script_1", "target": "notify_1" }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for security alerts channel"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/waf-detection.json b/backend/scripts/seed-templates/waf-detection.json
deleted file mode 100644
index c3a93d48..00000000
--- a/backend/scripts/seed-templates/waf-detection.json
+++ /dev/null
@@ -1,95 +0,0 @@
-{
- "_metadata": {
- "name": "WAF Detection",
- "description": "Detect Web Application Firewalls protecting target websites using wafw00f. Identifies and fingerprints WAF vendors across your web assets, helping security teams adjust testing strategies and inventory WAF coverage. Results are summarized and delivered via Slack.",
- "category": "web-security",
- "tags": ["waf", "detection", "firewall", "wafw00f", "reconnaissance"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "WAF Detection",
- "description": "Detect and fingerprint Web Application Firewalls on target URLs, then report findings via Slack.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "web-security",
- "tags": ["waf", "detection", "firewall", "wafw00f", "reconnaissance"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "WAF Detection",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start WAF Detection", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "wafw00f_1",
- "type": "sentris.wafw00f.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Detect WAF Presence",
- "config": {
- "params": {
- "findAll": true,
- "verbose": false
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "summarize_results",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Summarize WAF Results",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Save WAF Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#security-ops" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-wafw00f_1", "source": "trigger_1", "target": "wafw00f_1" },
- { "id": "wafw00f_1-summarize_results", "source": "wafw00f_1", "target": "summarize_results" },
- {
- "id": "summarize_results-artifact_report",
- "source": "summarize_results",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for WAF detection alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/web-application-scan.json b/backend/scripts/seed-templates/web-application-scan.json
deleted file mode 100644
index ea06e79b..00000000
--- a/backend/scripts/seed-templates/web-application-scan.json
+++ /dev/null
@@ -1,98 +0,0 @@
-{
- "_metadata": {
- "name": "Web Application Scan",
- "description": "End-to-end web application security workflow that probes targets for live HTTP services with HTTPX, crawls discovered hosts for URLs using Katana, and scans all crawled endpoints for vulnerabilities with Nuclei. Results are compiled into an artifact report and delivered via Slack. Ideal for scheduled external web-app assessments.",
- "category": "web-security",
- "tags": ["web", "vulnerability", "scanning", "httpx", "katana", "nuclei"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Web Application Scan",
- "description": "Probe live HTTP services, crawl URLs, scan for vulnerabilities, and deliver a security report.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "web-security",
- "tags": ["web", "vulnerability", "scanning", "httpx", "katana", "nuclei"],
- "entryPoint": "trigger_1",
- "nodeCount": 6,
- "edgeCount": 5
- },
- "graph": {
- "name": "Web Application Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start Web Scan", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "httpx_1",
- "type": "sentris.httpx.scan",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Probe Live HTTP Services",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "katana_1",
- "type": "sentris.katana.run",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Crawl Discovered Hosts",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "nuclei_1",
- "type": "sentris.nuclei.scan",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Scan for Vulnerabilities",
- "config": {
- "params": { "severity": "medium,high,critical" },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Generate Scan Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1600, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#web-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-httpx_1", "source": "trigger_1", "target": "httpx_1" },
- { "id": "httpx_1-katana_1", "source": "httpx_1", "target": "katana_1" },
- { "id": "katana_1-nuclei_1", "source": "katana_1", "target": "nuclei_1" },
- { "id": "nuclei_1-artifact_report", "source": "nuclei_1", "target": "artifact_report" },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for web security scan alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json b/backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json
index adcd00f6..4e485455 100644
--- a/backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json
+++ b/backend/scripts/seed-templates/web-attack-surface-quick-win-hunt.json
@@ -104,7 +104,7 @@
"params": {
"depth": 2,
"headless": false,
- "timeout": 300,
+ "timeout": 30,
"scope": "strict"
},
"inputOverrides": {}
@@ -119,18 +119,25 @@
"label": "Run Safe Quick Checks",
"config": {
"params": {
- "rateLimit": 75,
- "concurrency": 15,
+ "rateLimit": 10,
+ "concurrency": 3,
"timeout": 8,
- "retries": 1,
+ "retries": 0,
"includeRaw": false,
"followRedirects": true,
- "updateTemplates": false,
+ "updateTemplates": true,
"disableHttpx": true,
"severityFilter": ["info", "low", "medium", "high", "critical"]
},
"inputOverrides": {
- "templatePaths": ["http/exposures/", "http/misconfiguration/", "http/takeovers/"]
+ "templateIds": [
+ "cors-misconfig",
+ "generic-env",
+ "http-missing-security-headers",
+ "robots-txt",
+ "swagger-api",
+ "openapi"
+ ]
}
}
}
diff --git a/backend/scripts/seed-templates/web-crawl-discovery.json b/backend/scripts/seed-templates/web-crawl-discovery.json
deleted file mode 100644
index 65d15ea5..00000000
--- a/backend/scripts/seed-templates/web-crawl-discovery.json
+++ /dev/null
@@ -1,96 +0,0 @@
-{
- "_metadata": {
- "name": "Web Crawl Discovery",
- "description": "Crawl target websites using Katana to discover endpoints, JavaScript files, API routes, and forms. Results are formatted into a structured report and delivered via Slack. Use this template as a first step to map the web attack surface before vulnerability scanning.",
- "category": "web-security",
- "tags": ["crawler", "discovery", "katana", "web", "urls"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Web Crawl Discovery",
- "description": "Crawl websites with Katana to discover endpoints and hidden routes, then report via Slack.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "web-security",
- "tags": ["crawler", "discovery", "katana", "web", "urls"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "Web Crawl Discovery",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start Web Crawl", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "katana_1",
- "type": "sentris.katana.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Crawl Target Sites",
- "config": {
- "params": {
- "depth": 3,
- "scope": "strict",
- "headless": false
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "format_results",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Format Discovery Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Save Crawl Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#security-ops" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-katana_1", "source": "trigger_1", "target": "katana_1" },
- { "id": "katana_1-format_results", "source": "katana_1", "target": "format_results" },
- {
- "id": "format_results-artifact_report",
- "source": "format_results",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for web crawl discovery alerts"
- }
- ]
-}
diff --git a/backend/scripts/seed-templates/web-fuzzing-scan.json b/backend/scripts/seed-templates/web-fuzzing-scan.json
deleted file mode 100644
index 83ed5970..00000000
--- a/backend/scripts/seed-templates/web-fuzzing-scan.json
+++ /dev/null
@@ -1,97 +0,0 @@
-{
- "_metadata": {
- "name": "Web Fuzzing Scan",
- "description": "Automated web directory and endpoint fuzzing workflow using ffuf. Discovers hidden paths, files, and API endpoints on target web applications. Results are formatted into a structured report and delivered via Slack. Ideal for pre-engagement reconnaissance and web application assessments.",
- "category": "web-security",
- "tags": ["web", "fuzzing", "discovery", "ffuf", "reconnaissance"],
- "author": "sentris-team",
- "version": "1.0.0"
- },
- "manifest": {
- "name": "Web Fuzzing Scan",
- "description": "Fuzz web paths and directories to discover hidden content, then report findings via Slack.",
- "version": "1.0.0",
- "author": "sentris-team",
- "category": "web-security",
- "tags": ["web", "fuzzing", "discovery", "ffuf", "reconnaissance"],
- "entryPoint": "trigger_1",
- "nodeCount": 5,
- "edgeCount": 4
- },
- "graph": {
- "name": "Web Fuzzing Scan",
- "nodes": [
- {
- "id": "trigger_1",
- "type": "core.workflow.entrypoint",
- "position": { "x": 100, "y": 250 },
- "data": { "label": "Start Fuzzing Scan", "config": { "params": {}, "inputOverrides": {} } }
- },
- {
- "id": "ffuf_1",
- "type": "sentris.ffuf.run",
- "position": { "x": 400, "y": 250 },
- "data": {
- "label": "Fuzz Web Paths",
- "config": {
- "params": {
- "method": "GET",
- "rate": 100,
- "filterStatus": "404",
- "timeout": 300
- },
- "inputOverrides": {}
- }
- }
- },
- {
- "id": "format_results",
- "type": "core.logic.script",
- "position": { "x": 700, "y": 250 },
- "data": {
- "label": "Format Fuzzing Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "artifact_report",
- "type": "core.artifact.writer",
- "position": { "x": 1000, "y": 250 },
- "data": {
- "label": "Save Fuzzing Report",
- "config": { "params": {}, "inputOverrides": {} }
- }
- },
- {
- "id": "notify_results",
- "type": "core.notification.slack",
- "position": { "x": 1300, "y": 250 },
- "data": {
- "label": "Notify Security Team",
- "config": { "params": { "channel": "#web-security" }, "inputOverrides": {} }
- }
- }
- ],
- "edges": [
- { "id": "trigger_1-ffuf_1", "source": "trigger_1", "target": "ffuf_1" },
- { "id": "ffuf_1-format_results", "source": "ffuf_1", "target": "format_results" },
- {
- "id": "format_results-artifact_report",
- "source": "format_results",
- "target": "artifact_report"
- },
- {
- "id": "artifact_report-notify_results",
- "source": "artifact_report",
- "target": "notify_results"
- }
- ]
- },
- "requiredSecrets": [
- {
- "name": "slack_webhook_url",
- "type": "string",
- "description": "Slack incoming webhook URL for fuzzing scan alerts"
- }
- ]
-}
diff --git a/backend/src/templates/__tests__/seed-templates.spec.ts b/backend/src/templates/__tests__/seed-templates.spec.ts
index 258d03b6..ed31b77a 100644
--- a/backend/src/templates/__tests__/seed-templates.spec.ts
+++ b/backend/src/templates/__tests__/seed-templates.spec.ts
@@ -10,9 +10,16 @@ const newTemplateFiles = [
'bug-bounty-recon-triage.json',
'cve-impact-research-brief.json',
'exposed-service-cve-mapper.json',
+ 'npm-dependency-cve-hunt.json',
'web-attack-surface-quick-win-hunt.json',
];
+function runTemplateScript(code: string, input: unknown): T {
+ const executable = code.replace('export function script', 'function script');
+ const script = new Function(`${executable}; return script;`)() as (input: unknown) => T;
+ return script(input);
+}
+
describe('new seed templates', () => {
for (const fileName of newTemplateFiles) {
it(`${fileName} exists and contains a valid workflow graph`, () => {
@@ -60,4 +67,224 @@ describe('new seed templates', () => {
expect((runtimeInputs as unknown[]).length).toBeGreaterThan(0);
});
}
+
+ it('cve-impact-research-brief includes source status in the report assembly inputs', () => {
+ const filePath = join(seedTemplatesDir, 'cve-impact-research-brief.json');
+ const template = JSON.parse(readFileSync(filePath, 'utf8'));
+ const graph = template.graph;
+ const nvdNode = graph.nodes.find((node: { id: string }) => node.id === 'query_nvd');
+ const assembleNode = graph.nodes.find(
+ (node: { id: string }) => node.id === 'assemble_research_brief',
+ );
+ const variables = assembleNode.data.config.params.variables.map(
+ (variable: { name: string }) => variable.name,
+ );
+ const edges = graph.edges.map(
+ (edge: { source: string; target: string; sourceHandle?: string; targetHandle?: string }) =>
+ `${edge.source}:${edge.sourceHandle}->${edge.target}:${edge.targetHandle}`,
+ );
+
+ expect(nvdNode?.type).toBe('sentris.nvd.cve.query');
+ expect(graph.nodes.some((node: { id: string }) => node.id === 'build_nvd_url')).toBe(false);
+ expect(graph.nodes.some((node: { id: string }) => node.id === 'fetch_nvd')).toBe(false);
+ expect(variables).toEqual(
+ expect.arrayContaining(['nvdStatus', 'nvdStatusText', 'kevStatus', 'kevStatusText']),
+ );
+ expect(edges).toEqual(
+ expect.arrayContaining([
+ 'trigger_1:cveId->query_nvd:cveIds',
+ 'query_nvd:status->assemble_research_brief:nvdStatus',
+ 'query_nvd:statusText->assemble_research_brief:nvdStatusText',
+ 'fetch_kev:status->assemble_research_brief:kevStatus',
+ 'fetch_kev:statusText->assemble_research_brief:kevStatusText',
+ ]),
+ );
+ });
+
+ it('exposed-service-cve-mapper includes NVD source status in candidate ranking inputs', () => {
+ const filePath = join(seedTemplatesDir, 'exposed-service-cve-mapper.json');
+ const template = JSON.parse(readFileSync(filePath, 'utf8'));
+ const graph = template.graph;
+ const nvdNode = graph.nodes.find((node: { id: string }) => node.id === 'query_nvd_candidates');
+ const rankNode = graph.nodes.find((node: { id: string }) => node.id === 'rank_cve_candidates');
+ const variables = rankNode.data.config.params.variables.map(
+ (variable: { name: string }) => variable.name,
+ );
+ const edges = graph.edges.map(
+ (edge: { source: string; target: string; sourceHandle?: string; targetHandle?: string }) =>
+ `${edge.source}:${edge.sourceHandle}->${edge.target}:${edge.targetHandle}`,
+ );
+
+ expect(nvdNode?.type).toBe('sentris.nvd.cve.query');
+ expect(graph.nodes.some((node: { id: string }) => node.id === 'fetch_nvd_candidates')).toBe(
+ false,
+ );
+ expect(variables).toEqual(expect.arrayContaining(['nvdStatus', 'nvdStatusText']));
+ expect(edges).toEqual(
+ expect.arrayContaining([
+ 'build_cve_queries:keywordSearch->query_nvd_candidates:keywordSearch',
+ 'query_nvd_candidates:status->rank_cve_candidates:nvdStatus',
+ 'query_nvd_candidates:statusText->rank_cve_candidates:nvdStatusText',
+ ]),
+ );
+ });
+
+ it('exposed-service-cve-mapper gives broad NVD keyword searches enough time', () => {
+ const filePath = join(seedTemplatesDir, 'exposed-service-cve-mapper.json');
+ const template = JSON.parse(readFileSync(filePath, 'utf8'));
+ const nvdNode = template.graph.nodes.find(
+ (node: { id: string }) => node.id === 'query_nvd_candidates',
+ );
+
+ expect(nvdNode.data.config.params.timeoutMs).toBeGreaterThanOrEqual(60_000);
+ });
+
+ it('exposed-service-cve-mapper keeps broad NVD keyword pages bounded', () => {
+ const filePath = join(seedTemplatesDir, 'exposed-service-cve-mapper.json');
+ const template = JSON.parse(readFileSync(filePath, 'utf8'));
+ const nvdNode = template.graph.nodes.find(
+ (node: { id: string }) => node.id === 'query_nvd_candidates',
+ );
+
+ expect(nvdNode.data.config.params.resultsPerPage).toBeLessThanOrEqual(5);
+ });
+
+ it('exposed-service-cve-mapper strips versions from technology fingerprints before NVD search', () => {
+ const filePath = join(seedTemplatesDir, 'exposed-service-cve-mapper.json');
+ const template = JSON.parse(readFileSync(filePath, 'utf8'));
+ const buildNode = template.graph.nodes.find(
+ (node: { id: string }) => node.id === 'build_cve_queries',
+ );
+
+ const result = runTemplateScript<{ keywordSearch: string; fingerprints: { keyword: string } }>(
+ buildNode.data.config.params.code,
+ {
+ httpResponses: [
+ {
+ url: 'http://scanme.nmap.org:80',
+ statusCode: 200,
+ title: 'Go ahead and ScanMe!',
+ technologies: ['Apache HTTP Server:2.4.7', 'Ubuntu'],
+ },
+ ],
+ },
+ );
+
+ expect(result.keywordSearch).toBe('Apache HTTP Server');
+ expect(result.fingerprints.keyword).toBe('Apache HTTP Server');
+ });
+
+ it('exposed-service-cve-mapper prioritizes matching high severity recent CVEs', () => {
+ const filePath = join(seedTemplatesDir, 'exposed-service-cve-mapper.json');
+ const template = JSON.parse(readFileSync(filePath, 'utf8'));
+ const rankNode = template.graph.nodes.find(
+ (node: { id: string }) => node.id === 'rank_cve_candidates',
+ );
+
+ const result = runTemplateScript<{
+ report: {
+ summary: { topCandidate?: string; highestSeverity?: string };
+ candidates: {
+ id: string;
+ priorityScore?: number;
+ priorityReasons?: string[];
+ severity?: string;
+ cvssScore?: number;
+ }[];
+ };
+ }>(rankNode.data.config.params.code, {
+ fingerprints: {
+ keyword: 'Apache HTTP Server',
+ technologies: ['Apache HTTP Server', 'Ubuntu'],
+ services: [{ url: 'http://scanme.nmap.org:80', statusCode: 200 }],
+ },
+ nvdStatus: 200,
+ nvdStatusText: 'OK',
+ nvdData: {
+ vulnerabilities: [
+ {
+ cve: {
+ id: 'CVE-2000-0001',
+ published: '2000-01-01T00:00:00.000',
+ lastModified: '2000-01-02T00:00:00.000',
+ descriptions: [
+ {
+ lang: 'en',
+ value: 'Apache HTTP Server information disclosure with limited impact.',
+ },
+ ],
+ metrics: {
+ cvssMetricV31: [
+ {
+ cvssData: { baseScore: 5.3, baseSeverity: 'MEDIUM' },
+ },
+ ],
+ },
+ references: { referenceData: [] },
+ },
+ },
+ {
+ cve: {
+ id: 'CVE-2026-9999',
+ published: '2026-02-01T00:00:00.000',
+ lastModified: '2026-02-02T00:00:00.000',
+ descriptions: [
+ {
+ lang: 'en',
+ value: 'Unrelated nginx remote code execution vulnerability.',
+ },
+ ],
+ metrics: {
+ cvssMetricV31: [
+ {
+ cvssData: { baseScore: 10, baseSeverity: 'CRITICAL' },
+ },
+ ],
+ },
+ references: { referenceData: [] },
+ },
+ },
+ {
+ cve: {
+ id: 'CVE-2026-0002',
+ published: '2026-01-01T00:00:00.000',
+ lastModified: '2026-01-02T00:00:00.000',
+ descriptions: [
+ {
+ lang: 'en',
+ value:
+ 'Apache HTTP Server request smuggling remote code execution vulnerability.',
+ },
+ ],
+ metrics: {
+ cvssMetricV31: [
+ {
+ cvssData: { baseScore: 9.8, baseSeverity: 'CRITICAL' },
+ },
+ ],
+ },
+ references: {
+ referenceData: [{ url: 'https://example.com/apache-http-server-rce' }],
+ },
+ },
+ },
+ ],
+ },
+ });
+
+ expect(result.report.candidates[0].id).toBe('CVE-2026-0002');
+ expect(result.report.candidates[0].severity).toBe('CRITICAL');
+ expect(result.report.candidates[0].cvssScore).toBe(9.8);
+ expect(result.report.candidates[0].priorityScore).toBeGreaterThan(
+ result.report.candidates[1].priorityScore ?? 0,
+ );
+ expect(result.report.candidates[0].priorityReasons).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining('fingerprint keyword'),
+ expect.stringContaining('critical severity'),
+ ]),
+ );
+ expect(result.report.summary.topCandidate).toBe('CVE-2026-0002');
+ expect(result.report.summary.highestSeverity).toBe('CRITICAL');
+ });
});
diff --git a/backend/src/templates/__tests__/templates.service.spec.ts b/backend/src/templates/__tests__/templates.service.spec.ts
index 2fdf8f8a..e32ff2b5 100644
--- a/backend/src/templates/__tests__/templates.service.spec.ts
+++ b/backend/src/templates/__tests__/templates.service.spec.ts
@@ -53,14 +53,15 @@ function makeTemplate(overrides: Record = {}) {
}
const seedTemplateDir = join(import.meta.dir, '../../../scripts/seed-templates');
-const bugBountyCveTemplateFiles = [
+const securityTemplateFiles = [
'bug-bounty-recon-triage.json',
'cve-impact-research-brief.json',
'exposed-service-cve-mapper.json',
+ 'npm-dependency-cve-hunt.json',
'web-attack-surface-quick-win-hunt.json',
] as const;
-function loadSeedTemplate(fileName: (typeof bugBountyCveTemplateFiles)[number]) {
+function loadSeedTemplate(fileName: (typeof securityTemplateFiles)[number]) {
return JSON.parse(readFileSync(join(seedTemplateDir, fileName), 'utf8')) as {
_metadata: {
name: string;
@@ -194,8 +195,8 @@ describe('TemplateService', () => {
});
});
- it('creates workflows from the bug bounty and CVE research seed templates', async () => {
- for (const fileName of bugBountyCveTemplateFiles) {
+ it('creates workflows from the security seed templates', async () => {
+ for (const fileName of securityTemplateFiles) {
const seedTemplate = loadSeedTemplate(fileName);
const tpl = makeTemplate({
id: `tpl-${fileName}`,
@@ -223,9 +224,9 @@ describe('TemplateService', () => {
expect(result.workflow.id).toBe(`wf-${fileName}`);
}
- expect(workflowsService.create).toHaveBeenCalledTimes(bugBountyCveTemplateFiles.length);
+ expect(workflowsService.create).toHaveBeenCalledTimes(securityTemplateFiles.length);
expect(templatesRepository.incrementPopularity).toHaveBeenCalledTimes(
- bugBountyCveTemplateFiles.length,
+ securityTemplateFiles.length,
);
});
diff --git a/docs/superpowers/plans/2026-06-21-osv-dependency-component.md b/docs/superpowers/plans/2026-06-21-osv-dependency-component.md
new file mode 100644
index 00000000..005e6210
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-21-osv-dependency-component.md
@@ -0,0 +1,169 @@
+# OSV Dependency Component Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a reusable OSV dependency advisory component and move the npm dependency template off embedded OSV scripts.
+
+**Architecture:** Implement a focused inline worker component at `worker/src/components/security/osv.ts`. It parses package specs, calls OSV `querybatch`, optionally hydrates advisories with `/v1/vulns/{id}`, emits structured findings plus analytics-ready results, and is registered from `worker/src/components/index.ts`. Then update `backend/scripts/seed-templates/npm-dependency-cve-hunt.json` to use the component and keep the existing artifact writer for report persistence.
+
+**Tech Stack:** TypeScript, Bun test, Sentris component SDK, OSV REST API, existing seed-template compiler tests and template live-audit harness.
+
+---
+
+### Task 1: Add OSV Component Tests
+
+**Files:**
+
+- Create: `worker/src/components/security/__tests__/osv.test.ts`
+
+- [ ] **Step 1: Write failing tests**
+
+Create tests that import `../osv`, retrieve `sentris.osv.query`, and verify:
+
+```ts
+expect(componentRegistry.get('sentris.osv.query')).toBeDefined();
+expect(parsePackageSpec('lodash@4.17.20', 'npm')).toEqual({
+ spec: 'lodash@4.17.20',
+ name: 'lodash',
+ version: '4.17.20',
+ ecosystem: 'npm',
+});
+expect(parsePackageSpec('@scope/pkg@1.2.3', 'npm')).toEqual({
+ spec: '@scope/pkg@1.2.3',
+ name: '@scope/pkg',
+ version: '1.2.3',
+ ecosystem: 'npm',
+});
+expect(inferOsvSeverity({ database_specific: { severity: 'HIGH' } })).toBe('high');
+expect(extractFixedVersions(sampleAdvisory)).toEqual(['4.17.21']);
+```
+
+Mock `global.fetch` so execute sees one `querybatch` response and one hydrated advisory response:
+
+```ts
+const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (url) => {
+ const text = String(url);
+ if (text.endsWith('/v1/querybatch')) {
+ return new Response(
+ JSON.stringify({
+ results: [{ vulns: [{ id: 'GHSA-test', modified: '2026-01-01T00:00:00Z' }] }],
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ );
+ }
+ if (text.endsWith('/v1/vulns/GHSA-test')) {
+ return new Response(JSON.stringify(sampleAdvisory), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+ return new Response('not found', { status: 404 });
+});
+```
+
+Assert execute returns one finding with `id`, `cves`, `fixedVersions`, `severity`, and one analytics result whose `scanner` is `osv`.
+
+- [ ] **Step 2: Verify tests fail**
+
+Run: `bun --cwd worker test src/components/security/__tests__/osv.test.ts`
+
+Expected: FAIL because `worker/src/components/security/osv.ts` does not exist.
+
+### Task 2: Implement OSV Component
+
+**Files:**
+
+- Create: `worker/src/components/security/osv.ts`
+- Modify: `worker/src/components/index.ts`
+
+- [ ] **Step 1: Add component implementation**
+
+Implement:
+
+```ts
+export function parsePackageSpec(spec: string, defaultEcosystem: string): NormalizedPackage | null;
+export function inferOsvSeverity(vuln: unknown): 'critical' | 'high' | 'medium' | 'low' | 'unknown';
+export function extractFixedVersions(vuln: unknown): string[];
+export const definition = defineComponent({ id: 'sentris.osv.query', ... });
+```
+
+The component inputs are `packageSpecs`; parameters are `ecosystem`, `severityFloor`, `hydrateAdvisories`, `maxAdvisoriesPerPackage`, and `includeUnknownSeverity`. Execution posts `{ queries }` to `https://api.osv.dev/v1/querybatch`, hydrates advisories when configured, builds `findings`, `summary`, `rawResults`, and `results`, and maps analytics severity to `critical | high | medium | low | info`.
+
+- [ ] **Step 2: Register component**
+
+Add `import './security/osv';` near the other security component imports in `worker/src/components/index.ts`.
+
+- [ ] **Step 3: Verify component tests pass**
+
+Run: `bun --cwd worker test src/components/security/__tests__/osv.test.ts`
+
+Expected: PASS.
+
+### Task 3: Update NPM Template To Use OSV Component
+
+**Files:**
+
+- Modify: `backend/scripts/seed-templates/npm-dependency-cve-hunt.json`
+- Test: `backend/src/templates/__tests__/seed-templates.spec.ts`
+
+- [ ] **Step 1: Replace embedded OSV scripts**
+
+Make the graph:
+
+```text
+trigger_1 -> osv_query -> assemble_dependency_report -> artifact_report
+```
+
+`osv_query` uses `sentris.osv.query`, receives `packageSpecs`, and has params:
+
+```json
+{
+ "ecosystem": "npm",
+ "severityFloor": "medium",
+ "hydrateAdvisories": true,
+ "maxAdvisoriesPerPackage": 50,
+ "includeUnknownSeverity": true
+}
+```
+
+The assemble script only packages `findings`, `summary`, `packages`, and `researchNotes` into the existing report shape.
+
+- [ ] **Step 2: Verify seed graph compiles**
+
+Run: `bun --cwd backend test src/templates/__tests__/seed-templates.spec.ts`
+
+Expected: PASS.
+
+### Task 4: Live Verification And Broad Checks
+
+**Files:**
+
+- Use existing: `scripts/template-library-live-audit.ts`
+
+- [ ] **Step 1: Upsert active local template row**
+
+Run: `$env:DATABASE_URL='postgresql://sentris:sentris@localhost:5433/sentris_instance_0'; bun --cwd backend scripts/seed-templates.ts`
+
+Expected: existing templates skipped. If this script only skips existing template rows, update the active DB row for `NPM Dependency CVE Hunt` from the seed file before live audit.
+
+- [ ] **Step 2: Run focused live audit**
+
+Run: `$env:TEMPLATE_AUDIT_NAMES='NPM Dependency CVE Hunt'; bun scripts/template-library-live-audit.ts`
+
+Expected: `KEEP COMPLETED` and one JSON artifact with nonzero findings.
+
+- [ ] **Step 3: Run verification suite**
+
+Run:
+
+```bash
+bun --cwd worker test src/components/security/__tests__/osv.test.ts
+bun --cwd backend test src/templates/__tests__/seed-templates.spec.ts
+bun --cwd backend test src/templates/__tests__/templates.service.spec.ts
+bun run typecheck
+bun run lint
+git diff --check -- worker/src/components/security/osv.ts worker/src/components/security/__tests__/osv.test.ts worker/src/components/index.ts backend/scripts/seed-templates/npm-dependency-cve-hunt.json backend/src/templates/__tests__/seed-templates.spec.ts backend/src/templates/__tests__/templates.service.spec.ts scripts/template-library-live-audit.ts
+docker ps --filter label=sentris.runId --format "{{.ID}} {{.Image}} {{.Status}}"
+```
+
+Expected: tests/typecheck exit 0, lint exits 0 with only existing warnings, scoped diff check exits 0, and no leftover Sentris-labeled containers.
diff --git a/docs/superpowers/plans/2026-06-21-template-library-live-verification.md b/docs/superpowers/plans/2026-06-21-template-library-live-verification.md
new file mode 100644
index 00000000..4c69bdd4
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-21-template-library-live-verification.md
@@ -0,0 +1,143 @@
+# Template Library Live Verification Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Live-verify every Template Library seed template against the local Sentris instance, then remove or consolidate templates that are broken, unusable, or too low-value to keep.
+
+**Architecture:** Add a repeatable audit harness that reads seed templates, compares them with the running API, creates workflows through the same template-use endpoint as the UI, runs templates where live inputs and credentials are available, and writes JSON/Markdown evidence. Use the audit output to make scoped edits to seed template JSON files and active DB template rows.
+
+**Tech Stack:** Bun/Node, NestJS API at `http://localhost:3211/api/v1`, Temporal-backed workflow runs, seed templates in `backend/scripts/seed-templates/*.json`.
+
+---
+
+### Task 1: Build The Live Audit Harness
+
+**Files:**
+
+- Create: `scripts/template-library-live-audit.ts`
+
+- [ ] **Step 1: Add API helpers**
+
+Create helpers for `GET /templates`, `POST /templates/:id/use`, `POST /workflows/:id/run`, `GET /workflows/runs/:runId/status`, `GET /workflows/runs/:runId/artifacts`, `GET /workflows/runs/:runId/node-io`, and workflow cleanup.
+
+- [ ] **Step 2: Add template classification**
+
+Classify templates as:
+
+- `live-run`: no external secrets and enough runtime inputs to execute safely.
+- `structure-only`: no runtime inputs or cannot accept target/user data.
+- `credential-gated`: requires real third-party credentials or Slack delivery.
+
+- [ ] **Step 3: Add controlled fixtures**
+
+Use safe public/controlled targets:
+
+- `CVE-2024-3094` for CVE research.
+- `https://host.docker.internal:18443/api/health` for web/API flows.
+- `example.com` for DNS/recon flows.
+- `scanme.nmap.org` for bounded port/service checks.
+
+- [ ] **Step 4: Write audit output**
+
+Write output under a timestamped temp directory:
+
+- `template-live-audit.json`
+- `template-live-audit.md`
+- per-run traces and node I/O summaries
+
+### Task 2: Run The Audit
+
+**Files:**
+
+- Execute: `scripts/template-library-live-audit.ts`
+
+- [ ] **Step 1: Confirm services**
+
+Run:
+
+```powershell
+Invoke-RestMethod http://localhost:3211/api/v1/health
+Invoke-RestMethod http://localhost:3211/api/v1/health/ready
+```
+
+Expected: both return `status: "ok"`.
+
+- [ ] **Step 2: Run harness**
+
+Run:
+
+```powershell
+bun scripts/template-library-live-audit.ts
+```
+
+Expected: harness audits all active templates and completes with a report path.
+
+- [ ] **Step 3: Inspect failures**
+
+For each failed live-run template, read trace/node I/O, identify root cause, and decide whether to fix or prune.
+
+### Task 3: Clean Up Low-Value Templates
+
+**Files:**
+
+- Modify/delete: `backend/scripts/seed-templates/*.json`
+- Possibly add: `backend/scripts/seed-templates/_retired.json` only if the existing seeding system needs retirement metadata.
+
+- [ ] **Step 1: Remove templates that are static demos**
+
+Delete templates that cannot be used without hard-coded assumptions and do not expose runtime inputs, unless they represent a high-value credential-backed workflow.
+
+- [ ] **Step 2: Consolidate duplicates**
+
+Prefer one useful version when multiple templates cover the same flow with only superficial differences, for example generic web scan vs bug bounty web quick-win scan.
+
+- [ ] **Step 3: Update active DB rows**
+
+Because the local seed service skips existing rows, apply equivalent changes to the active instance database so `/templates` matches the edited seed files.
+
+### Task 4: Verify
+
+**Files:**
+
+- Test: `backend/src/templates/__tests__/seed-templates.spec.ts`
+- Test: affected worker/component tests if runner behavior changes
+
+- [ ] **Step 1: Validate seed templates**
+
+Run:
+
+```powershell
+bun --cwd backend test src/templates/__tests__/seed-templates.spec.ts
+```
+
+Expected: all template validation tests pass.
+
+- [ ] **Step 2: Typecheck**
+
+Run:
+
+```powershell
+bun run typecheck
+```
+
+Expected: exit code 0.
+
+- [ ] **Step 3: Lint**
+
+Run:
+
+```powershell
+bun run lint
+```
+
+Expected: exit code 0. Existing warnings may remain; no new errors.
+
+- [ ] **Step 4: Re-run live audit**
+
+Run:
+
+```powershell
+bun scripts/template-library-live-audit.ts
+```
+
+Expected: remaining live-run templates complete, credential-gated templates are explicitly classified, and no active template is left in a broken/static-demo state.
diff --git a/packages/component-sdk/src/__tests__/har-builder.test.ts b/packages/component-sdk/src/__tests__/har-builder.test.ts
new file mode 100644
index 00000000..55f4c320
--- /dev/null
+++ b/packages/component-sdk/src/__tests__/har-builder.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from 'bun:test';
+
+import { buildHarResponse } from '../http/har-builder';
+import type { HttpResponseLike } from '../http/types';
+
+describe('buildHarResponse', () => {
+ it('does not wait for clone stream cancellation after truncating response capture', async () => {
+ const encoder = new TextEncoder();
+ let readCount = 0;
+
+ const body = new ReadableStream({
+ pull(controller) {
+ readCount += 1;
+ controller.enqueue(encoder.encode(`${readCount}`.repeat(20)));
+ },
+ cancel: () => new Promise(() => {}),
+ });
+
+ const response = {
+ status: 200,
+ statusText: 'OK',
+ headers: new Headers({ 'content-type': 'text/plain' }),
+ text: async () => '',
+ clone: () => response,
+ body,
+ } as unknown as HttpResponseLike;
+
+ const result = await Promise.race([
+ buildHarResponse(response, { maxResponseBodySize: 10 }),
+ new Promise<'timed out'>((resolve) => setTimeout(() => resolve('timed out'), 50)),
+ ]);
+
+ expect(result).not.toBe('timed out');
+ if (result !== 'timed out') {
+ expect(result.content.text).toBe('1111111111');
+ expect(result.content.size).toBeGreaterThan(10);
+ }
+ });
+});
diff --git a/packages/component-sdk/src/__tests__/http-instrumentation.test.ts b/packages/component-sdk/src/__tests__/http-instrumentation.test.ts
index d558343a..19f45240 100644
--- a/packages/component-sdk/src/__tests__/http-instrumentation.test.ts
+++ b/packages/component-sdk/src/__tests__/http-instrumentation.test.ts
@@ -42,6 +42,43 @@ describe('HTTP instrumentation', () => {
}
});
+ it('preserves response body when HAR capture truncates a large response', async () => {
+ const recorded: TraceEvent[] = [];
+ const trace: ITraceService = {
+ record: (event) => {
+ recorded.push(event);
+ },
+ };
+
+ const context = createExecutionContext({
+ runId: 'run-http-large',
+ componentRef: 'test.http',
+ trace,
+ });
+
+ const largeBody = 'a'.repeat(60 * 1024);
+ const originalFetch = globalThis.fetch;
+ const mockFetch = Object.assign(
+ async () =>
+ new Response(largeBody, {
+ status: 200,
+ headers: { 'Content-Type': 'text/plain' },
+ }),
+ { preconnect: () => {} },
+ ) as typeof fetch;
+ globalThis.fetch = mockFetch;
+
+ try {
+ const response = await context.http.fetch('https://example.com/large');
+ await expect(response.text()).resolves.toBe(largeBody);
+
+ const responseEvent = recorded.find((event) => event.type === 'HTTP_RESPONSE_RECEIVED');
+ expect(responseEvent?.data).toBeDefined();
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
it('emits HTTP_REQUEST_ERROR when fetch fails', async () => {
const recorded: TraceEvent[] = [];
const trace: ITraceService = {
diff --git a/packages/component-sdk/src/__tests__/runner-docker-pull.test.ts b/packages/component-sdk/src/__tests__/runner-docker-pull.test.ts
new file mode 100644
index 00000000..b91482c4
--- /dev/null
+++ b/packages/component-sdk/src/__tests__/runner-docker-pull.test.ts
@@ -0,0 +1,89 @@
+import { EventEmitter } from 'node:events';
+import { describe, expect, it, mock, vi } from 'bun:test';
+import { createExecutionContext } from '../context';
+
+const spawnCalls: string[][] = [];
+
+const dockerSpawn = vi.fn((_: string, args: string[]) => {
+ spawnCalls.push(args);
+
+ const proc = new EventEmitter() as EventEmitter & {
+ stdout: EventEmitter;
+ stderr: EventEmitter;
+ stdin: { write: ReturnType; end: ReturnType };
+ kill: ReturnType;
+ };
+
+ proc.stdout = new EventEmitter();
+ proc.stderr = new EventEmitter();
+ proc.stdin = {
+ write: vi.fn(),
+ end: vi.fn(),
+ };
+ proc.kill = vi.fn();
+
+ queueMicrotask(() => {
+ if (args[0] === 'image' && args[1] === 'inspect') {
+ proc.emit('close', 1);
+ return;
+ }
+
+ if (args[0] === 'pull') {
+ proc.stderr.emit('data', Buffer.from('Pulling fs layer\n'));
+ proc.emit('close', 0);
+ return;
+ }
+
+ if (args[0] === 'run') {
+ proc.stdout.emit('data', Buffer.from('{"ok":true}'));
+ proc.emit('close', 0);
+ return;
+ }
+
+ proc.emit('close', 0);
+ });
+
+ return proc;
+});
+
+mock.module('child_process', () => ({
+ spawn: dockerSpawn,
+}));
+
+const { runComponentWithRunner, stripAnsiCodes } = await import('../runner');
+
+describe('Docker image preparation', () => {
+ it('strips private-mode terminal control sequences from fallback output', () => {
+ expect(stripAnsiCodes('\x1B[?9001h\x1B[?1004h\x1B[?25lapi.example.com')).toBe(
+ 'api.example.com',
+ );
+ });
+
+ it('pulls a missing image before running the container without polluting output', async () => {
+ spawnCalls.length = 0;
+
+ const context = createExecutionContext({
+ runId: 'docker-pull-run',
+ componentRef: 'docker.pull',
+ });
+
+ const result = await runComponentWithRunner(
+ {
+ kind: 'docker',
+ image: 'example/scanner:latest',
+ command: ['scan'],
+ timeoutSeconds: 30,
+ },
+ async () => ({}),
+ {},
+ context,
+ );
+
+ expect(result).toEqual({ ok: true });
+ expect(spawnCalls.map((args) => args.slice(0, 3))).toEqual([
+ ['image', 'inspect', 'example/scanner:latest'],
+ ['pull', 'example/scanner:latest'],
+ ['run', '--rm', '-i'],
+ ]);
+ });
+});
diff --git a/packages/component-sdk/src/http/har-builder.ts b/packages/component-sdk/src/http/har-builder.ts
index 4074ebb2..85376e17 100644
--- a/packages/component-sdk/src/http/har-builder.ts
+++ b/packages/component-sdk/src/http/har-builder.ts
@@ -66,9 +66,7 @@ export function headersToHar(headers: HttpHeaders | Record): Har
export function maskHeaders(headers: HarHeader[], sensitive: string[]): HarHeader[] {
const sensitiveSet = normalizeSensitiveHeaders(sensitive);
return headers.map((header) =>
- sensitiveSet.has(header.name.toLowerCase())
- ? { ...header, value: '***' }
- : header,
+ sensitiveSet.has(header.name.toLowerCase()) ? { ...header, value: '***' } : header,
);
}
@@ -87,9 +85,7 @@ export function maskQueryParams(
): HarQueryString[] {
const sensitiveSet = new Set(sensitive.map((param) => param.toLowerCase()));
return queryParams.map((param) =>
- sensitiveSet.has(param.name.toLowerCase())
- ? { ...param, value: '***' }
- : param,
+ sensitiveSet.has(param.name.toLowerCase()) ? { ...param, value: '***' } : param,
);
}
@@ -113,10 +109,7 @@ export function maskUrlQueryParams(url: string, sensitive: string[]): string {
}
}
-export function truncateBody(
- body: string,
- maxSize: number,
-): { text: string; truncated: boolean } {
+export function truncateBody(body: string, maxSize: number): { text: string; truncated: boolean } {
if (body.length <= maxSize) {
return { text: body, truncated: false };
}
@@ -202,13 +195,14 @@ export async function buildHarResponse(
chunks.push(chunk.slice(0, remaining));
bodyText += chunk.slice(0, remaining);
truncated = true;
- // Cancel the stream to avoid reading remaining data
- await reader.cancel();
+ // Do not await clone cancellation: Node's fetch tee can wait until
+ // the original response body is consumed, delaying callers.
+ void reader.cancel().catch(() => undefined);
break;
}
} else {
truncated = true;
- await reader.cancel();
+ void reader.cancel().catch(() => undefined);
break;
}
}
diff --git a/packages/component-sdk/src/runner.ts b/packages/component-sdk/src/runner.ts
index a26cf8d0..4b950e1c 100644
--- a/packages/component-sdk/src/runner.ts
+++ b/packages/component-sdk/src/runner.ts
@@ -12,16 +12,17 @@ import { ContainerError, TimeoutError, ValidationError, ConfigurationError } fro
* Docker containers and PTY output often contain color/control codes
* that pollute structured output (JSON parsing, line splitting).
*/
-function stripAnsiCodes(text: string): string {
- // Covers SGR (colors), cursor movement, and other CSI sequences
- return text.replace(/\x1B(?:[@-Z\\-_]|\[[0-9;]*[ -/]*[@-~])/g, '');
+export function stripAnsiCodes(text: string): string {
+ return text
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
}
// Standard output file path inside the container
const CONTAINER_OUTPUT_PATH = '/sentris-output';
const OUTPUT_FILENAME = 'result.json';
-type PtySpawn = typeof import('node-pty')['spawn'];
+type PtySpawn = (typeof import('node-pty'))['spawn'];
let cachedPtySpawn: PtySpawn | null = null;
let cachedDockerPath: string | null = null;
@@ -47,13 +48,13 @@ export async function resolveDockerPath(context?: ExecutionContext): Promise {
cachedPtySpawn = mod.spawn;
return cachedPtySpawn;
} catch (error) {
- console.warn('[Docker][PTY] node-pty module not available:', error instanceof Error ? error.message : error);
+ console.warn(
+ '[Docker][PTY] node-pty module not available:',
+ error instanceof Error ? error.message : error,
+ );
return null;
}
}
+async function runDockerSetupCommand(
+ args: string[],
+ context: ExecutionContext,
+ timeoutSeconds: number,
+): Promise<{ stdout: string; stderr: string }> {
+ const dockerPath = await resolveDockerPath(context);
+
+ return new Promise((resolve, reject) => {
+ let stdout = '';
+ let stderr = '';
+
+ const proc = spawn(dockerPath, args, {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: process.env,
+ });
+
+ const timeout = setTimeout(() => {
+ proc.kill();
+ reject(
+ new TimeoutError(
+ `Docker setup command timed out after ${timeoutSeconds}s`,
+ timeoutSeconds * 1000,
+ { details: { dockerArgs: formatArgs(args) } },
+ ),
+ );
+ }, timeoutSeconds * 1000);
+
+ proc.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+
+ proc.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ proc.on('error', (error) => {
+ clearTimeout(timeout);
+ reject(
+ new ContainerError(`Failed to run Docker setup command: ${error.message}`, {
+ cause: error,
+ details: { dockerArgs: formatArgs(args) },
+ }),
+ );
+ });
+
+ proc.on('close', (code) => {
+ clearTimeout(timeout);
+ if (code !== 0) {
+ reject(
+ new ContainerError(`Docker setup command failed with exit code ${code}`, {
+ details: { exitCode: code, stdout, stderr, dockerArgs: formatArgs(args) },
+ }),
+ );
+ return;
+ }
+
+ resolve({ stdout, stderr });
+ });
+ });
+}
+
+async function ensureDockerImageAvailable(
+ runner: DockerRunnerConfig,
+ context: ExecutionContext,
+): Promise {
+ const setupTimeoutSeconds = Math.max(300, runner.timeoutSeconds ?? 300);
+ const inspectArgs = ['image', 'inspect', runner.image];
+
+ try {
+ await runDockerSetupCommand(inspectArgs, context, setupTimeoutSeconds);
+ return;
+ } catch {
+ context.emitProgress(`Pulling Docker image: ${runner.image}`);
+ }
+
+ const pullArgs = ['pull'];
+ if (runner.platform && runner.platform.trim().length > 0) {
+ pullArgs.push('--platform', runner.platform);
+ }
+ pullArgs.push(runner.image);
+
+ try {
+ await runDockerSetupCommand(pullArgs, context, setupTimeoutSeconds);
+ } catch (error) {
+ throw new ContainerError(`Failed to pull Docker image: ${runner.image}`, {
+ cause: error instanceof Error ? error : undefined,
+ details: { image: runner.image },
+ });
+ }
+}
+
export async function runComponentInline(
execute: (params: I, context: ExecutionContext) => Promise,
params: I,
@@ -123,7 +218,18 @@ async function runComponentInDocker(
params: I,
context: ExecutionContext,
): Promise {
- const { image, command, entrypoint, env = {}, network = 'none', platform, containerName, volumes, timeoutSeconds = 300, detached } = runner;
+ const {
+ image,
+ command,
+ entrypoint,
+ env = {},
+ network = 'none',
+ platform,
+ containerName,
+ volumes,
+ timeoutSeconds = 300,
+ detached,
+ } = runner;
const memoryLimit = runner.memoryLimit ?? '512m';
const cpuLimit = runner.cpuLimit ?? '1';
const pidsLimit = runner.pidsLimit ?? 256;
@@ -139,19 +245,27 @@ async function runComponentInDocker(
try {
// Write inputs to file instead of passing via env or stdin
await writeFile(hostInputPath, JSON.stringify(params));
+ await ensureDockerImageAvailable(runner, context);
const dockerArgs = [
'run',
'--rm',
'-i',
- '--network', network,
- '--memory', memoryLimit,
- '--cpus', cpuLimit,
- '--pids-limit', String(pidsLimit),
- '--label', `sentris.runId=${context.runId}`,
- '--label', `sentris.nodeRef=${context.componentRef}`,
+ '--network',
+ network,
+ '--memory',
+ memoryLimit,
+ '--cpus',
+ cpuLimit,
+ '--pids-limit',
+ String(pidsLimit),
+ '--label',
+ `sentris.runId=${context.runId}`,
+ '--label',
+ `sentris.nodeRef=${context.componentRef}`,
// Mount the directory containing both input and output
- '-v', `${outputDir}:${CONTAINER_OUTPUT_PATH}`,
+ '-v',
+ `${outputDir}:${CONTAINER_OUTPUT_PATH}`,
];
if (containerName) {
@@ -190,43 +304,56 @@ async function runComponentInDocker(
dockerArgs.push(image, ...command);
-
const useTerminal = Boolean(context.terminalCollector);
let capturedStdout = '';
if (runner.detached) {
// For detached mode, we use -d instead of -i and return the container ID
- const detachedArgs = dockerArgs.map(arg => arg === '-i' ? '-d' : arg);
+ const detachedArgs = dockerArgs.map((arg) => (arg === '-i' ? '-d' : arg));
if (!detachedArgs.includes('-d')) {
detachedArgs.splice(1, 0, '-d');
}
// In detached mode, keep --rm only when explicitly requested
- const persistentArgs = runner.autoRemove ? detachedArgs : detachedArgs.filter(arg => arg !== '--rm');
-
- capturedStdout = await runDockerWithStandardIO(persistentArgs, params, context, timeoutSeconds, runner.stdinJson, true);
+ const persistentArgs = runner.autoRemove
+ ? detachedArgs
+ : detachedArgs.filter((arg) => arg !== '--rm');
+
+ capturedStdout = await runDockerWithStandardIO(
+ persistentArgs,
+ params,
+ context,
+ timeoutSeconds,
+ runner.stdinJson,
+ true,
+ );
// In detached mode, we return the container ID as part of a specialized output
return {
containerId: capturedStdout.trim(),
status: 'running',
- endpoint: env.ENDPOINT || `http://localhost:${env.PORT || 8080}`
+ endpoint: env.ENDPOINT || `http://localhost:${env.PORT || 8080}`,
} as unknown as O;
}
if (useTerminal) {
// Remove -i flag for PTY mode (stdin not needed with TTY)
- const argsWithoutStdin = dockerArgs.filter(arg => arg !== '-i');
+ const argsWithoutStdin = dockerArgs.filter((arg) => arg !== '-i');
if (!argsWithoutStdin.includes('-t')) {
argsWithoutStdin.splice(2, 0, '-t');
}
// NEVER write JSON to stdin in PTY mode - it pollutes the terminal output
capturedStdout = await runDockerWithPty(argsWithoutStdin, params, context, timeoutSeconds);
} else {
- capturedStdout = await runDockerWithStandardIO(dockerArgs, params, context, timeoutSeconds, runner.stdinJson);
+ capturedStdout = await runDockerWithStandardIO(
+ dockerArgs,
+ params,
+ context,
+ timeoutSeconds,
+ runner.stdinJson,
+ );
}
-
// Read output from file (with stdout fallback for legacy components)
return await readOutputFromFile(hostOutputPath, capturedStdout, context);
} finally {
@@ -240,7 +367,7 @@ async function runComponentInDocker(
/**
* Read component output from the mounted output file.
* If file doesn't exist, falls back to stdout parsing for backwards compatibility.
- *
+ *
* @param filePath Path to the output file
* @param stdout Captured stdout as fallback for legacy components
* @param context Execution context for logging
@@ -248,7 +375,7 @@ async function runComponentInDocker(
async function readOutputFromFile(
filePath: string,
stdout: string,
- context: ExecutionContext
+ context: ExecutionContext,
): Promise {
// First, try to read from the output file (preferred method)
try {
@@ -276,7 +403,9 @@ async function readOutputFromFile(
// Strip ANSI escape codes before parsing — Docker/PTY output often contains
// color codes that break JSON parsing and pollute line-based output.
const cleanStdout = stripAnsiCodes(stdout);
- context.logger.info(`[Docker] No output file found, using stdout fallback (${cleanStdout.length} bytes)`);
+ context.logger.info(
+ `[Docker] No output file found, using stdout fallback (${cleanStdout.length} bytes)`,
+ );
// Try to parse stdout as JSON
try {
@@ -314,9 +443,15 @@ async function runDockerWithStandardIO(
const timeout = setTimeout(() => {
proc.kill();
- reject(new TimeoutError(`Docker container timed out after ${timeoutSeconds}s`, timeoutSeconds * 1000, {
- details: { dockerArgs: formatArgs(dockerArgs) },
- }));
+ reject(
+ new TimeoutError(
+ `Docker container timed out after ${timeoutSeconds}s`,
+ timeoutSeconds * 1000,
+ {
+ details: { dockerArgs: formatArgs(dockerArgs) },
+ },
+ ),
+ );
}, timeoutSeconds * 1000);
const proc = spawn(dockerPath, dockerArgs, {
@@ -324,8 +459,6 @@ async function runDockerWithStandardIO(
env: process.env,
});
-
-
let stdout = '';
let stderr = '';
@@ -355,7 +488,7 @@ async function runDockerWithStandardIO(
const chunk = data.toString();
stderr += chunk;
const isProgress = isDockerProgressMessage(chunk);
- const level = isProgress ? 'info' as const : 'error' as const;
+ const level = isProgress ? ('info' as const) : ('error' as const);
const logEntry = {
runId: context.runId,
nodeRef: context.componentRef,
@@ -383,10 +516,12 @@ async function runDockerWithStandardIO(
proc.on('error', (error) => {
clearTimeout(timeout);
context.logger.error(`[Docker] Failed to start: ${error.message}`);
- reject(new ContainerError(`Failed to start Docker container: ${error.message}`, {
- cause: error,
- details: { dockerArgs: formatArgs(dockerArgs) },
- }));
+ reject(
+ new ContainerError(`Failed to start Docker container: ${error.message}`, {
+ cause: error,
+ details: { dockerArgs: formatArgs(dockerArgs) },
+ }),
+ );
});
proc.on('close', (code) => {
@@ -403,9 +538,11 @@ async function runDockerWithStandardIO(
data: { exitCode: code, stderr: stderr.slice(0, 500) },
});
- reject(new ContainerError(`Docker container failed with exit code ${code}: ${stderr}`, {
- details: { exitCode: code, stderr, stdout, dockerArgs: formatArgs(dockerArgs) },
- }));
+ reject(
+ new ContainerError(`Docker container failed with exit code ${code}: ${stderr}`, {
+ details: { exitCode: code, stderr, stdout, dockerArgs: formatArgs(dockerArgs) },
+ }),
+ );
return;
}
@@ -425,10 +562,12 @@ async function runDockerWithStandardIO(
} catch (e) {
clearTimeout(timeout);
proc.kill();
- reject(new ValidationError(`Failed to write input to Docker container: ${e}`, {
- cause: e as Error,
- details: { inputType: typeof params },
- }));
+ reject(
+ new ValidationError(`Failed to write input to Docker container: ${e}`, {
+ cause: e as Error,
+ details: { inputType: typeof params },
+ }),
+ );
}
} else {
// Close stdin immediately if stdinJson is false
@@ -452,7 +591,7 @@ async function runDockerWithPty(
if (!spawnPty) {
context.logger.warn('[Docker][PTY] node-pty unavailable; falling back to standard IO');
// Remove -t flag before falling back to standard IO (stdin is not a TTY)
- const argsWithoutTty = dockerArgs.filter(arg => arg !== '-t');
+ const argsWithoutTty = dockerArgs.filter((arg) => arg !== '-t');
return runDockerWithStandardIO(argsWithoutTty, params, context, timeoutSeconds);
}
@@ -477,13 +616,16 @@ async function runDockerWithPty(
dockerPath,
pathEnv: process.env.PATH,
cwd: process.cwd(),
- error: error instanceof Error ? {
- message: error.message,
- stack: error.stack,
- name: error.name,
- // @ts-ignore
- code: error.code,
- } : String(error)
+ error:
+ error instanceof Error
+ ? {
+ message: error.message,
+ stack: error.stack,
+ name: error.name,
+ // @ts-ignore
+ code: error.code,
+ }
+ : String(error),
};
context.logger.warn(
@@ -500,13 +642,17 @@ async function runDockerWithPty(
return;
}
-
-
const timeout = setTimeout(() => {
ptyProcess.kill();
- reject(new TimeoutError(`Docker container timed out after ${timeoutSeconds}s`, timeoutSeconds * 1000, {
- details: { dockerArgs: formatArgs(dockerArgs) },
- }));
+ reject(
+ new TimeoutError(
+ `Docker container timed out after ${timeoutSeconds}s`,
+ timeoutSeconds * 1000,
+ {
+ details: { dockerArgs: formatArgs(dockerArgs) },
+ },
+ ),
+ );
}, timeoutSeconds * 1000);
// NEVER write JSON to stdin in PTY mode - it pollutes the terminal output
@@ -529,16 +675,15 @@ async function runDockerWithPty(
data: { exitCode },
});
- reject(new ContainerError(
- `Docker PTY execution failed with exit code ${exitCode}`,
- {
+ reject(
+ new ContainerError(`Docker PTY execution failed with exit code ${exitCode}`, {
details: {
exitCode,
stdout,
dockerArgs: formatArgs(dockerArgs),
},
- },
- ));
+ }),
+ );
return;
}
diff --git a/scripts/__tests__/template-library-live-audit-utils.test.ts b/scripts/__tests__/template-library-live-audit-utils.test.ts
new file mode 100644
index 00000000..22c97456
--- /dev/null
+++ b/scripts/__tests__/template-library-live-audit-utils.test.ts
@@ -0,0 +1,150 @@
+import { describe, expect, it } from 'bun:test';
+import {
+ getNodeIoWarningSignals,
+ renderTemplateAuditMarkdown,
+ summarizeNodeIoNode,
+ waitForNodeIoEvidence,
+} from '../template-library-live-audit-utils';
+
+describe('template library live audit helpers', () => {
+ it('waits for node I/O ingestion to reach the expected node count', async () => {
+ const sleeps: number[] = [];
+ const responses = [
+ { runId: 'run-1', nodes: [] },
+ { runId: 'run-1', nodes: [{ nodeRef: 'trigger_1' }] },
+ {
+ runId: 'run-1',
+ nodes: [{ nodeRef: 'trigger_1' }, { nodeRef: 'osv_query' }],
+ },
+ ];
+
+ const result = await waitForNodeIoEvidence({
+ runId: 'run-1',
+ expectedNodeCount: 2,
+ timeoutMs: 1000,
+ pollIntervalMs: 25,
+ sleep: async (ms) => {
+ sleeps.push(ms);
+ },
+ fetchNodeIo: async () => responses.shift() ?? { runId: 'run-1', nodes: [] },
+ });
+
+ expect(result.nodes).toHaveLength(2);
+ expect(sleeps).toEqual([25, 25]);
+ });
+
+ it('summarizes output keys when node outputs are a JSON string', () => {
+ const summary = summarizeNodeIoNode({
+ nodeRef: 'query_nvd_candidates',
+ componentId: 'sentris.nvd.cve.query',
+ status: 'completed',
+ durationMs: 1250,
+ outputs: JSON.stringify({
+ ok: true,
+ status: 200,
+ vulnerabilities: [],
+ }),
+ outputsSpilled: true,
+ outputsTruncated: false,
+ inputsSpilled: false,
+ inputsTruncated: false,
+ });
+
+ expect(summary).toEqual({
+ nodeRef: 'query_nvd_candidates',
+ componentId: 'sentris.nvd.cve.query',
+ status: 'completed',
+ durationMs: 1250,
+ errorMessage: null,
+ inputKeys: [],
+ outputKeys: ['ok', 'status', 'vulnerabilities'],
+ warnings: [],
+ inputsSpilled: false,
+ inputsTruncated: false,
+ outputsSpilled: true,
+ outputsTruncated: false,
+ });
+ });
+
+ it('extracts warning signals from node outputs', () => {
+ const summary = summarizeNodeIoNode({
+ nodeRef: 'query_nvd_candidates',
+ componentId: 'sentris.nvd.cve.query',
+ status: 'completed',
+ outputs: {
+ ok: false,
+ status: 503,
+ statusText: 'Service Unavailable',
+ warnings: ['NVD CVE query unavailable: Service Unavailable'],
+ },
+ });
+
+ expect(summary.warnings).toEqual(['NVD CVE query unavailable: Service Unavailable']);
+ expect(getNodeIoWarningSignals([summary])).toEqual([
+ 'query_nvd_candidates: NVD CVE query unavailable: Service Unavailable',
+ ]);
+ });
+
+ it('keeps truncation flags when serialized node outputs cannot be parsed', () => {
+ const summary = summarizeNodeIoNode({
+ nodeRef: 'rank_cve_candidates',
+ status: 'completed',
+ outputs: '{"report":',
+ outputsSpilled: true,
+ outputsTruncated: true,
+ });
+
+ expect(summary.outputKeys).toEqual([]);
+ expect(summary.outputsSpilled).toBe(true);
+ expect(summary.outputsTruncated).toBe(true);
+ });
+
+ it('renders node I/O evidence in the audit markdown report', () => {
+ const markdown = renderTemplateAuditMarkdown({
+ apiBase: 'http://127.0.0.1:3211/api/v1',
+ outputRoot: 'C:/tmp/audit',
+ generatedAt: '2026-06-21T04:00:00.000Z',
+ results: [
+ {
+ templateId: 'tpl-1',
+ templateName: 'Exposed Service CVE Mapper',
+ seedFile: 'exposed-service-cve-mapper.json',
+ category: 'cve-research',
+ components: ['sentris.nvd.cve.query'],
+ requiredSecrets: [],
+ runtimeInputs: [],
+ classification: 'live-run',
+ createOk: true,
+ runAttempted: true,
+ terminalStatus: 'COMPLETED',
+ artifactsCount: 1,
+ recommendation: 'keep',
+ rationale: 'Live execution completed and produced at least one artifact.',
+ nodeIo: [
+ {
+ nodeRef: 'query_nvd_candidates',
+ componentId: 'sentris.nvd.cve.query',
+ status: 'completed',
+ durationMs: 1250,
+ errorMessage: null,
+ inputKeys: ['keywordSearch'],
+ outputKeys: ['ok', 'status', 'vulnerabilities'],
+ warnings: [],
+ inputsSpilled: false,
+ inputsTruncated: false,
+ outputsSpilled: true,
+ outputsTruncated: false,
+ },
+ ],
+ },
+ ],
+ });
+
+ expect(markdown).toContain('## Node I/O Evidence');
+ expect(markdown).toContain('### Exposed Service CVE Mapper');
+ expect(markdown).toContain('query_nvd_candidates');
+ expect(markdown).toContain('sentris.nvd.cve.query');
+ expect(markdown).toContain('ok, status, vulnerabilities');
+ expect(markdown).toContain('outputs spilled');
+ });
+});
diff --git a/scripts/template-library-live-audit-utils.ts b/scripts/template-library-live-audit-utils.ts
new file mode 100644
index 00000000..952e0047
--- /dev/null
+++ b/scripts/template-library-live-audit-utils.ts
@@ -0,0 +1,287 @@
+export interface NodeIoEvidenceResponse {
+ runId?: string;
+ nodes?: Record[];
+ [key: string]: unknown;
+}
+
+export interface NodeIoNodeSummary {
+ nodeRef: string;
+ componentId?: string;
+ status?: string;
+ durationMs?: number | null;
+ errorMessage?: string | null;
+ inputKeys: string[];
+ outputKeys: string[];
+ warnings: string[];
+ inputsSpilled: boolean;
+ inputsTruncated: boolean;
+ outputsSpilled: boolean;
+ outputsTruncated: boolean;
+}
+
+export interface TemplateAuditMarkdownResult {
+ templateId: string;
+ templateName: string;
+ seedFile: string | null;
+ category: string | null;
+ components: string[];
+ requiredSecrets: string[];
+ runtimeInputs: unknown[];
+ classification: string;
+ createOk: boolean;
+ createError?: string;
+ runAttempted: boolean;
+ runStartOk?: boolean;
+ runStartError?: string;
+ terminalStatus?: string;
+ statusError?: string;
+ artifactsCount?: number;
+ nodeIo?: NodeIoNodeSummary[];
+ recommendation: string;
+ rationale: string;
+}
+
+export interface RenderTemplateAuditMarkdownOptions {
+ apiBase: string;
+ outputRoot: string;
+ generatedAt: string;
+ results: TemplateAuditMarkdownResult[];
+}
+
+export interface WaitForNodeIoEvidenceOptions {
+ runId: string;
+ expectedNodeCount?: number;
+ timeoutMs: number;
+ pollIntervalMs: number;
+ fetchNodeIo: () => Promise;
+ sleep?: (ms: number) => Promise;
+}
+
+function defaultSleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function hasEnoughNodeEvidence(
+ response: NodeIoEvidenceResponse,
+ expectedNodeCount: number,
+): boolean {
+ return Array.isArray(response.nodes) && response.nodes.length >= expectedNodeCount;
+}
+
+function parseRecord(value: unknown): Record | null {
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
+ return value as Record;
+ }
+
+ if (typeof value !== 'string') return null;
+
+ try {
+ const parsed = JSON.parse(value);
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
+ ? (parsed as Record)
+ : null;
+ } catch {
+ return null;
+ }
+}
+
+function getBoolean(value: unknown): boolean {
+ return value === true;
+}
+
+function getStringArray(value: unknown): string[] {
+ if (!Array.isArray(value)) return [];
+ return value.map(String).map((item) => item.trim()).filter(Boolean);
+}
+
+function addWarnings(target: Set, value: unknown): void {
+ for (const warning of getStringArray(value)) {
+ target.add(warning);
+ }
+}
+
+function collectNodeWarnings(outputs: Record | null): string[] {
+ if (!outputs) return [];
+
+ const warnings = new Set();
+ addWarnings(warnings, outputs.warnings);
+
+ const report = parseRecord(outputs.report);
+ addWarnings(warnings, report?.warnings);
+
+ const summary = parseRecord(outputs.summary);
+ addWarnings(warnings, summary?.warnings);
+
+ return Array.from(warnings);
+}
+
+export function summarizeNodeIoNode(node: Record): NodeIoNodeSummary {
+ const inputs = parseRecord(node.inputs);
+ const outputs = parseRecord(node.outputs);
+
+ return {
+ nodeRef: String(node.nodeRef ?? ''),
+ componentId: typeof node.componentId === 'string' ? node.componentId : undefined,
+ status: typeof node.status === 'string' ? node.status : undefined,
+ durationMs: typeof node.durationMs === 'number' ? node.durationMs : null,
+ errorMessage: typeof node.errorMessage === 'string' ? node.errorMessage : null,
+ inputKeys: inputs ? Object.keys(inputs) : [],
+ outputKeys: outputs ? Object.keys(outputs) : [],
+ warnings: collectNodeWarnings(outputs),
+ inputsSpilled: getBoolean(node.inputsSpilled),
+ inputsTruncated: getBoolean(node.inputsTruncated),
+ outputsSpilled: getBoolean(node.outputsSpilled),
+ outputsTruncated: getBoolean(node.outputsTruncated),
+ };
+}
+
+export function getNodeIoWarningSignals(nodes: NodeIoNodeSummary[]): string[] {
+ const signals = new Set();
+ for (const node of nodes) {
+ const nodeLabel = node.nodeRef || node.componentId || 'node';
+ for (const warning of node.warnings) {
+ signals.add(`${nodeLabel}: ${warning}`);
+ }
+ }
+ return Array.from(signals);
+}
+
+function escapeMarkdownTable(value: unknown): string {
+ return String(value ?? '').replace(/\|/g, '\\|');
+}
+
+function renderNodeFlags(node: NodeIoNodeSummary): string {
+ const flags: string[] = [];
+ if (node.inputsSpilled) flags.push('inputs spilled');
+ if (node.inputsTruncated) flags.push('inputs truncated');
+ if (node.outputsSpilled) flags.push('outputs spilled');
+ if (node.outputsTruncated) flags.push('outputs truncated');
+ return flags.join(', ') || '-';
+}
+
+function renderKeyList(keys: string[]): string {
+ return keys.length > 0 ? keys.join(', ') : '-';
+}
+
+function renderWarnings(warnings: string[]): string {
+ return warnings.length > 0 ? warnings.join('; ') : '-';
+}
+
+export function renderTemplateAuditMarkdown({
+ apiBase,
+ outputRoot,
+ generatedAt,
+ results,
+}: RenderTemplateAuditMarkdownOptions): string {
+ const counts = results.reduce>((acc, result) => {
+ acc[result.recommendation] = (acc[result.recommendation] ?? 0) + 1;
+ return acc;
+ }, {});
+
+ const lines: string[] = [
+ '# Template Library Live Audit',
+ '',
+ `API base: \`${apiBase}\``,
+ `Generated: ${generatedAt}`,
+ `Output directory: \`${outputRoot}\``,
+ '',
+ '## Summary',
+ '',
+ `- Templates audited: ${results.length}`,
+ `- Keep: ${counts.keep ?? 0}`,
+ `- Fix: ${counts.fix ?? 0}`,
+ `- Consolidate: ${counts.consolidate ?? 0}`,
+ `- Delete: ${counts.delete ?? 0}`,
+ `- Review: ${counts.review ?? 0}`,
+ '',
+ '## Results',
+ '',
+ '| Template | Class | Run | Artifacts | Recommendation | Rationale |',
+ '| --- | --- | --- | ---: | --- | --- |',
+ ];
+
+ for (const result of results) {
+ const run =
+ result.terminalStatus ??
+ (result.runStartError ? 'run failed to start' : result.runAttempted ? 'started' : 'not run');
+ lines.push(
+ `| ${escapeMarkdownTable(result.templateName)} | ${escapeMarkdownTable(
+ result.classification,
+ )} | ${escapeMarkdownTable(run)} | ${result.artifactsCount ?? 0} | ${escapeMarkdownTable(
+ result.recommendation,
+ )} | ${escapeMarkdownTable(result.rationale)} |`,
+ );
+ }
+
+ lines.push('', '## Node I/O Evidence', '');
+ for (const result of results) {
+ lines.push(`### ${result.templateName}`, '');
+ const nodes = result.nodeIo ?? [];
+ if (nodes.length === 0) {
+ lines.push('No node I/O evidence captured.', '');
+ continue;
+ }
+
+ lines.push(
+ '| Node | Component | Status | Duration | Input Keys | Output Keys | Flags | Warnings | Error |',
+ '| --- | --- | --- | ---: | --- | --- | --- | --- | --- |',
+ );
+ for (const node of nodes) {
+ lines.push(
+ `| ${escapeMarkdownTable(node.nodeRef)} | ${escapeMarkdownTable(
+ node.componentId ?? '-',
+ )} | ${escapeMarkdownTable(node.status ?? '-')} | ${node.durationMs ?? '-'} | ${escapeMarkdownTable(
+ renderKeyList(node.inputKeys),
+ )} | ${escapeMarkdownTable(renderKeyList(node.outputKeys))} | ${escapeMarkdownTable(
+ renderNodeFlags(node),
+ )} | ${escapeMarkdownTable(renderWarnings(node.warnings))} | ${escapeMarkdownTable(
+ node.errorMessage ?? '-',
+ )} |`,
+ );
+ }
+ lines.push('');
+ }
+
+ lines.push('## Credential-Gated Templates', '');
+ for (const result of results.filter((item) => item.requiredSecrets.length > 0)) {
+ lines.push(`- ${result.templateName}: ${result.requiredSecrets.join(', ')}`);
+ }
+
+ lines.push('', '## Delete Candidates', '');
+ for (const result of results.filter((item) => item.recommendation === 'delete')) {
+ lines.push(`- ${result.seedFile ?? result.templateName}: ${result.rationale}`);
+ }
+
+ lines.push('', '## Fix Candidates', '');
+ for (const result of results.filter((item) => item.recommendation === 'fix')) {
+ lines.push(`- ${result.seedFile ?? result.templateName}: ${result.rationale}`);
+ }
+
+ return `${lines.join('\n')}\n`;
+}
+
+export async function waitForNodeIoEvidence({
+ runId,
+ expectedNodeCount,
+ timeoutMs,
+ pollIntervalMs,
+ fetchNodeIo,
+ sleep = defaultSleep,
+}: WaitForNodeIoEvidenceOptions): Promise {
+ const targetNodeCount = Math.max(1, expectedNodeCount ?? 1);
+ const interval = Math.max(1, pollIntervalMs);
+ const maxAttempts = Math.max(1, Math.ceil(Math.max(0, timeoutMs) / interval) + 1);
+ let latest: NodeIoEvidenceResponse = { runId, nodes: [] };
+
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
+ latest = await fetchNodeIo();
+ if (hasEnoughNodeEvidence(latest, targetNodeCount)) {
+ return latest;
+ }
+ if (attempt < maxAttempts - 1) {
+ await sleep(interval);
+ }
+ }
+
+ return latest;
+}
diff --git a/scripts/template-library-live-audit.ts b/scripts/template-library-live-audit.ts
new file mode 100644
index 00000000..7ca4e2cc
--- /dev/null
+++ b/scripts/template-library-live-audit.ts
@@ -0,0 +1,661 @@
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import {
+ getNodeIoWarningSignals,
+ renderTemplateAuditMarkdown,
+ summarizeNodeIoNode,
+ waitForNodeIoEvidence,
+} from './template-library-live-audit-utils';
+
+type JsonObject = Record;
+
+interface RuntimeInput {
+ id: string;
+ label?: string;
+ type?: string;
+ required?: boolean;
+ description?: string;
+}
+
+interface RequiredSecret {
+ name: string;
+ type: string;
+ description?: string;
+}
+
+interface GraphNode {
+ id: string;
+ type?: string;
+ data?: {
+ label?: string;
+ config?: {
+ params?: JsonObject;
+ inputOverrides?: JsonObject;
+ };
+ };
+}
+
+interface SeedTemplate {
+ _metadata?: {
+ name?: string;
+ description?: string;
+ category?: string;
+ tags?: string[];
+ author?: string;
+ version?: string;
+ };
+ manifest?: {
+ name?: string;
+ category?: string;
+ tags?: string[];
+ };
+ graph?: {
+ name?: string;
+ nodes?: GraphNode[];
+ edges?: unknown[];
+ };
+ requiredSecrets?: RequiredSecret[];
+}
+
+interface ApiTemplate {
+ id: string;
+ name: string;
+ description?: string | null;
+ category?: string | null;
+ tags?: string[] | null;
+ graph?: SeedTemplate['graph'] | null;
+ requiredSecrets?: RequiredSecret[] | null;
+ path?: string | null;
+ repository?: string | null;
+}
+
+interface CreatedWorkflowResponse {
+ workflow?: {
+ id?: string;
+ };
+ templateId?: string;
+ templateName?: string;
+}
+
+interface RunStartResponse {
+ runId: string;
+ workflowId: string;
+ temporalRunId?: string;
+ status?: string;
+}
+
+interface RunStatusResponse {
+ status: string;
+ [key: string]: unknown;
+}
+
+interface NodeIoSummary {
+ nodeRef: string;
+ componentId?: string;
+ status?: string;
+ durationMs?: number | null;
+ errorMessage?: string | null;
+ inputKeys?: string[];
+ outputKeys?: string[];
+ warnings?: string[];
+ inputsSpilled?: boolean;
+ inputsTruncated?: boolean;
+ outputsSpilled?: boolean;
+ outputsTruncated?: boolean;
+}
+
+interface AuditResult {
+ templateId: string;
+ templateName: string;
+ seedFile: string | null;
+ category: string | null;
+ components: string[];
+ requiredSecrets: string[];
+ runtimeInputs: RuntimeInput[];
+ classification: 'live-run' | 'credential-gated' | 'run-start-probe' | 'create-only';
+ workflowId?: string;
+ createOk: boolean;
+ createError?: string;
+ runAttempted: boolean;
+ runStartOk?: boolean;
+ runStartError?: string;
+ runId?: string;
+ terminalStatus?: string;
+ statusError?: string;
+ artifactsCount?: number;
+ nodeIo?: NodeIoSummary[];
+ recommendation: 'keep' | 'fix' | 'consolidate' | 'delete' | 'review';
+ rationale: string;
+}
+
+const DEFAULT_INSTANCE = Number.parseInt(
+ process.env.SENTRIS_INSTANCE ?? process.env.E2E_INSTANCE ?? '0',
+ 10,
+);
+const API_BASE =
+ process.env.SENTRIS_API_BASE_URL ??
+ process.env.API_BASE ??
+ `http://127.0.0.1:${3211 + (Number.isFinite(DEFAULT_INSTANCE) ? DEFAULT_INSTANCE : 0) * 100}/api/v1`;
+const INTERNAL_TOKEN = process.env.SENTRIS_INTERNAL_TOKEN ?? 'local-internal-token';
+const ORG_ID = process.env.SENTRIS_ORG_ID ?? 'org_dev';
+const RUN_TIMEOUT_MS = Number.parseInt(process.env.TEMPLATE_AUDIT_TIMEOUT_MS ?? '420000', 10);
+const NODE_IO_CAPTURE_TIMEOUT_MS = Number.parseInt(
+ process.env.TEMPLATE_AUDIT_NODE_IO_TIMEOUT_MS ?? '30000',
+ 10,
+);
+const NODE_IO_CAPTURE_POLL_MS = Number.parseInt(
+ process.env.TEMPLATE_AUDIT_NODE_IO_POLL_MS ?? '1000',
+ 10,
+);
+const KEEP_WORKFLOWS = process.env.KEEP_AUDIT_WORKFLOWS === 'true';
+const OUTPUT_ROOT =
+ process.env.TEMPLATE_AUDIT_OUTPUT_DIR ??
+ join(tmpdir(), `sentris-template-live-audit-${new Date().toISOString().replace(/[:.]/g, '-')}`);
+const AUDIT_TEMPLATE_NAMES = new Set(
+ (process.env.TEMPLATE_AUDIT_NAMES ?? '')
+ .split(',')
+ .map((name) => name.trim())
+ .filter(Boolean),
+);
+
+const HEADERS = {
+ 'Content-Type': 'application/json',
+ 'x-internal-token': INTERNAL_TOKEN,
+ 'x-organization-id': ORG_ID,
+};
+
+const TERMINAL_STATUSES = new Set([
+ 'COMPLETED',
+ 'FAILED',
+ 'CANCELLED',
+ 'TERMINATED',
+ 'TIMED_OUT',
+ 'CONTINUED_AS_NEW',
+ 'UNKNOWN',
+]);
+
+const LIVE_INPUTS: Record = {
+ 'Bug Bounty Recon Triage': {
+ domains: ['example.com'],
+ authorizationNotes: 'Live audit fixture: public example domain, passive/bounded recon.',
+ },
+ 'CVE Impact Research Brief': {
+ cveId: 'CVE-2024-3094',
+ product: 'xz utils',
+ version: '5.6.1',
+ deploymentNotes: 'Live audit fixture for known public CVE research.',
+ },
+ 'Exposed Service CVE Mapper': {
+ targets: ['scanme.nmap.org'],
+ authorizationNotes: 'Live audit fixture: Nmap-provided scan target for bounded service checks.',
+ },
+ 'NPM Dependency CVE Hunt': {
+ packageSpecs: ['lodash@4.17.20', 'minimist@0.0.8', 'axios@0.21.1'],
+ researchNotes: 'Live audit fixture using public npm packages with known historical advisories.',
+ },
+ 'Web Attack Surface Quick Win Hunt': {
+ liveUrls: ['https://host.docker.internal:18443/api/health'],
+ outOfScopePaths: ['/logout', '/admin/delete'],
+ scanIntensity: 'safe',
+ },
+};
+
+function ensureOutputDir() {
+ mkdirSync(OUTPUT_ROOT, { recursive: true });
+}
+
+function sanitizeFileName(value: string): string {
+ return value
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-|-$/g, '')
+ .slice(0, 90);
+}
+
+async function apiFetch(path: string, init?: RequestInit): Promise {
+ const response = await fetch(`${API_BASE}${path}`, {
+ ...init,
+ headers: {
+ ...HEADERS,
+ ...(init?.headers ?? {}),
+ },
+ });
+
+ const text = await response.text();
+ let body: unknown = null;
+ if (text.trim().length > 0) {
+ try {
+ body = JSON.parse(text);
+ } catch {
+ body = text;
+ }
+ }
+
+ if (!response.ok) {
+ const message = typeof body === 'string' ? body : JSON.stringify(body);
+ throw new Error(`${response.status} ${response.statusText}: ${message}`);
+ }
+
+ return body as T;
+}
+
+function readSeedTemplates(): Map {
+ const seedDir = join(process.cwd(), 'backend', 'scripts', 'seed-templates');
+ const result = new Map();
+ const entries = Array.from(new Bun.Glob('*.json').scanSync(seedDir)).sort();
+
+ for (const file of entries) {
+ const template = JSON.parse(readFileSync(join(seedDir, file), 'utf8')) as SeedTemplate;
+ const name = template._metadata?.name ?? template.manifest?.name;
+ if (name) {
+ result.set(name, { file, template });
+ }
+ }
+
+ return result;
+}
+
+function getComponents(template: SeedTemplate | ApiTemplate): string[] {
+ const graph = template.graph;
+ const nodes = graph?.nodes ?? [];
+ return Array.from(new Set(nodes.map((node) => node.type).filter(Boolean) as string[])).sort();
+}
+
+function getRuntimeInputs(template: SeedTemplate | ApiTemplate): RuntimeInput[] {
+ const entry = template.graph?.nodes?.find((node) => node.type === 'core.workflow.entrypoint');
+ const raw = entry?.data?.config?.params?.runtimeInputs;
+ return Array.isArray(raw) ? (raw as RuntimeInput[]) : [];
+}
+
+function getEntryRuntimeInputState(
+ template: SeedTemplate | ApiTemplate,
+): 'missing' | 'empty' | 'present' {
+ const entry = template.graph?.nodes?.find((node) => node.type === 'core.workflow.entrypoint');
+ const raw = entry?.data?.config?.params?.runtimeInputs;
+ if (!Array.isArray(raw)) return 'missing';
+ return raw.length === 0 ? 'empty' : 'present';
+}
+
+function getRequiredSecretNames(template: SeedTemplate | ApiTemplate): string[] {
+ return (template.requiredSecrets ?? []).map((secret) => secret.name).filter(Boolean);
+}
+
+function hasUnmappedSlackNode(template: SeedTemplate | ApiTemplate): boolean {
+ return (template.graph?.nodes ?? []).some((node) => {
+ if (node.type !== 'core.notification.slack') return false;
+ const params = node.data?.config?.params ?? {};
+ const inputOverrides = node.data?.config?.inputOverrides ?? {};
+ const authType = params.authType ?? 'bot_token';
+ if (authType === 'webhook') return !inputOverrides.webhookUrl;
+ return !inputOverrides.slackToken;
+ });
+}
+
+function classifyTemplate(
+ template: ApiTemplate,
+ seed: SeedTemplate | undefined,
+): AuditResult['classification'] {
+ const runtimeInputs = getRuntimeInputs(seed ?? template);
+ const requiredSecrets = getRequiredSecretNames(seed ?? template);
+ if (LIVE_INPUTS[template.name] && requiredSecrets.length === 0) return 'live-run';
+ if (requiredSecrets.length > 0 && runtimeInputs.length > 0) return 'credential-gated';
+ if (requiredSecrets.length > 0 && runtimeInputs.length === 0) return 'run-start-probe';
+ return 'run-start-probe';
+}
+
+function analyzeRecommendation(
+ template: ApiTemplate,
+ seed: SeedTemplate | undefined,
+ result: Partial,
+): Pick {
+ const source = seed ?? template;
+ const runtimeState = getEntryRuntimeInputState(source);
+ const requiredSecrets = getRequiredSecretNames(source);
+ const components = getComponents(source);
+ const unmappedSlack = hasUnmappedSlackNode(source);
+ const nodeWarningSignals = getNodeIoWarningSignals(result.nodeIo ?? []);
+
+ if (result.terminalStatus === 'COMPLETED' && (result.artifactsCount ?? 0) > 0) {
+ if (nodeWarningSignals.length > 0) {
+ return {
+ recommendation: 'review',
+ rationale: `Live execution completed with artifact but emitted warnings: ${nodeWarningSignals
+ .slice(0, 3)
+ .join('; ')
+ .slice(0, 240)}`,
+ };
+ }
+
+ return {
+ recommendation: 'keep',
+ rationale: 'Live execution completed and produced at least one artifact.',
+ };
+ }
+
+ if (runtimeState === 'missing') {
+ return {
+ recommendation: 'delete',
+ rationale:
+ 'Entry point has no runtimeInputs configuration, so a user-created workflow cannot compile/run from the template.',
+ };
+ }
+
+ if (unmappedSlack) {
+ return {
+ recommendation: 'fix',
+ rationale:
+ 'Template has a Slack node with no connected/mapped Slack token or webhook input; remove optional notification or add real secret plumbing.',
+ };
+ }
+
+ if (requiredSecrets.length > 0) {
+ return {
+ recommendation: 'review',
+ rationale: `Credential-gated template requires: ${requiredSecrets.join(', ')}.`,
+ };
+ }
+
+ if (
+ components.includes('sentris.trivy.run') ||
+ components.includes('sentris.semgrep.run') ||
+ components.includes('sentris.ffuf.run') ||
+ components.includes('sentris.checkov.run')
+ ) {
+ return {
+ recommendation: 'fix',
+ rationale:
+ 'Scanner template needs user-facing runtime inputs for target code, repo, image, URL, wordlist, or IaC content.',
+ };
+ }
+
+ if (result.runStartError) {
+ return {
+ recommendation: 'fix',
+ rationale: result.runStartError.split('\n')[0].slice(0, 240),
+ };
+ }
+
+ return {
+ recommendation: 'review',
+ rationale: 'No terminal live result; review trace and template shape before retaining.',
+ };
+}
+
+async function maybeUseExistingHttpsFixture(): Promise {
+ // The previous live workflow harness leaves a local fixture on 18443 in some sessions.
+ // If it is absent, public HTTPS targets are still used by CVE/service flows.
+ const localFixture = 'https://localhost:18443/api/health';
+ try {
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+ const response = await fetch(localFixture, { signal: AbortSignal.timeout(2000) });
+ if (response.ok) {
+ console.log(`Detected HTTPS fixture: ${localFixture}`);
+ return;
+ }
+ } catch {
+ LIVE_INPUTS['Web Attack Surface Quick Win Hunt'] = {
+ liveUrls: ['https://example.com'],
+ outOfScopePaths: ['/logout', '/admin/delete'],
+ scanIntensity: 'safe',
+ };
+ console.log(
+ 'No local HTTPS fixture detected; Web quick-win audit will use https://example.com.',
+ );
+ }
+}
+
+async function pollRun(runId: string, timeoutMs: number): Promise {
+ const started = Date.now();
+ let last: RunStatusResponse | null = null;
+
+ while (Date.now() - started < timeoutMs) {
+ last = await apiFetch(`/workflows/runs/${runId}/status`);
+ if (TERMINAL_STATUSES.has(last.status)) return last;
+ await Bun.sleep(1500);
+ }
+
+ throw new Error(
+ `Run ${runId} did not reach a terminal state in ${timeoutMs}ms; last status ${last?.status ?? 'unknown'}`,
+ );
+}
+
+async function cancelRun(runId: string): Promise {
+ try {
+ await apiFetch(`/workflows/runs/${runId}/cancel`, { method: 'POST' });
+ } catch (error) {
+ console.warn(`Failed to cancel ${runId}: ${error instanceof Error ? error.message : error}`);
+ }
+}
+
+async function deleteWorkflow(workflowId: string): Promise {
+ if (KEEP_WORKFLOWS) return;
+ try {
+ await apiFetch(`/workflows/${workflowId}`, { method: 'DELETE' });
+ } catch (error) {
+ console.warn(
+ `Failed to delete audit workflow ${workflowId}: ${error instanceof Error ? error.message : error}`,
+ );
+ }
+}
+
+async function captureRunEvidence(
+ runId: string,
+ prefix: string,
+ expectedNodeCount?: number,
+): Promise<{
+ artifactsCount: number;
+ nodeIo: NodeIoSummary[];
+}> {
+ const [artifacts, nodeIo, trace] = await Promise.all([
+ apiFetch<{ artifacts?: unknown[] }>(`/workflows/runs/${runId}/artifacts`).catch((error) => ({
+ error: error instanceof Error ? error.message : String(error),
+ })),
+ waitForNodeIoEvidence({
+ runId,
+ expectedNodeCount,
+ timeoutMs: NODE_IO_CAPTURE_TIMEOUT_MS,
+ pollIntervalMs: NODE_IO_CAPTURE_POLL_MS,
+ fetchNodeIo: () =>
+ apiFetch<{ nodes?: Record[] }>(`/workflows/runs/${runId}/node-io`).catch(
+ (error) => ({
+ runId,
+ nodes: [],
+ error: error instanceof Error ? error.message : String(error),
+ }),
+ ),
+ }),
+ apiFetch(`/workflows/runs/${runId}/trace`).catch((error) => ({
+ error: error instanceof Error ? error.message : String(error),
+ })),
+ ]);
+
+ writeFileSync(join(OUTPUT_ROOT, `${prefix}.artifacts.json`), JSON.stringify(artifacts, null, 2));
+ writeFileSync(join(OUTPUT_ROOT, `${prefix}.node-io.json`), JSON.stringify(nodeIo, null, 2));
+ writeFileSync(join(OUTPUT_ROOT, `${prefix}.trace.json`), JSON.stringify(trace, null, 2));
+
+ const artifactList = Array.isArray((artifacts as { artifacts?: unknown[] }).artifacts)
+ ? (artifacts as { artifacts: unknown[] }).artifacts
+ : Array.isArray(artifacts)
+ ? (artifacts as unknown[])
+ : [];
+
+ const nodes = Array.isArray((nodeIo as { nodes?: unknown[] }).nodes)
+ ? (nodeIo as { nodes: Array> }).nodes
+ : [];
+
+ return {
+ artifactsCount: artifactList.length,
+ nodeIo: nodes.map((node) => summarizeNodeIoNode(node)),
+ };
+}
+
+async function auditTemplate(
+ template: ApiTemplate,
+ seedRecord: { file: string; template: SeedTemplate } | undefined,
+): Promise {
+ const seed = seedRecord?.template;
+ const source = seed ?? template;
+ const classification = classifyTemplate(template, seed);
+ const components = getComponents(source);
+ const requiredSecrets = getRequiredSecretNames(source);
+ const runtimeInputs = getRuntimeInputs(source);
+ const prefix = `${sanitizeFileName(template.name)}-${template.id.slice(0, 8)}`;
+
+ const base: AuditResult = {
+ templateId: template.id,
+ templateName: template.name,
+ seedFile: seedRecord?.file ?? null,
+ category: template.category ?? seed?._metadata?.category ?? null,
+ components,
+ requiredSecrets,
+ runtimeInputs,
+ classification,
+ createOk: false,
+ runAttempted: false,
+ recommendation: 'review',
+ rationale: 'Audit did not reach recommendation step.',
+ };
+
+ let workflowId: string | undefined;
+
+ try {
+ const created = await apiFetch(`/templates/${template.id}/use`, {
+ method: 'POST',
+ body: JSON.stringify({
+ workflowName: `Template Live Audit - ${template.name} - ${new Date().toISOString()}`,
+ }),
+ });
+ workflowId = created.workflow?.id;
+ base.workflowId = workflowId;
+ base.createOk = Boolean(workflowId);
+ if (!workflowId) {
+ base.createError = `Use-template response had no workflow id: ${JSON.stringify(created)}`;
+ const recommendation = analyzeRecommendation(template, seed, base);
+ return { ...base, ...recommendation };
+ }
+ } catch (error) {
+ base.createError = error instanceof Error ? error.message : String(error);
+ const recommendation = analyzeRecommendation(template, seed, base);
+ return { ...base, ...recommendation };
+ }
+
+ const shouldRun =
+ classification === 'live-run' ||
+ (classification === 'run-start-probe' && requiredSecrets.length === 0) ||
+ (classification === 'run-start-probe' && runtimeInputs.length === 0);
+
+ if (!shouldRun) {
+ await deleteWorkflow(workflowId);
+ const recommendation = analyzeRecommendation(template, seed, base);
+ return { ...base, ...recommendation };
+ }
+
+ const inputs = LIVE_INPUTS[template.name] ?? {};
+ base.runAttempted = true;
+
+ try {
+ const started = await apiFetch(`/workflows/${workflowId}/run`, {
+ method: 'POST',
+ body: JSON.stringify({ inputs }),
+ });
+ base.runStartOk = true;
+ base.runId = started.runId;
+
+ if (requiredSecrets.length > 0 && classification !== 'live-run') {
+ await cancelRun(started.runId);
+ base.terminalStatus = 'CANCELLED';
+ base.statusError =
+ 'Run unexpectedly started for a credential-gated template; cancelled to avoid external side effects.';
+ } else {
+ const status = await pollRun(started.runId, RUN_TIMEOUT_MS);
+ base.terminalStatus = status.status;
+ }
+
+ const expectedNodeCount = template.graph?.nodes?.length ?? seed?.graph?.nodes?.length;
+ const evidence = await captureRunEvidence(started.runId, prefix, expectedNodeCount);
+ base.artifactsCount = evidence.artifactsCount;
+ base.nodeIo = evidence.nodeIo;
+ } catch (error) {
+ base.runStartOk = false;
+ base.runStartError = error instanceof Error ? error.message : String(error);
+ } finally {
+ if (workflowId) {
+ await deleteWorkflow(workflowId);
+ }
+ }
+
+ const recommendation = analyzeRecommendation(template, seed, base);
+ return { ...base, ...recommendation };
+}
+
+async function main() {
+ ensureOutputDir();
+ await maybeUseExistingHttpsFixture();
+
+ console.log(`Template audit output: ${OUTPUT_ROOT}`);
+ console.log(`API base: ${API_BASE}`);
+
+ await apiFetch('/health');
+ await apiFetch('/health/ready');
+
+ const seedTemplates = readSeedTemplates();
+ const templates = await apiFetch('/templates');
+ const selectedTemplates =
+ AUDIT_TEMPLATE_NAMES.size > 0
+ ? templates.filter((template) => AUDIT_TEMPLATE_NAMES.has(template.name))
+ : templates;
+ const missingTemplateNames = Array.from(AUDIT_TEMPLATE_NAMES).filter(
+ (name) => !selectedTemplates.some((template) => template.name === name),
+ );
+ if (missingTemplateNames.length > 0) {
+ throw new Error(`Requested template(s) not found: ${missingTemplateNames.join(', ')}`);
+ }
+
+ const results: AuditResult[] = [];
+
+ for (const template of selectedTemplates.sort((a, b) => a.name.localeCompare(b.name))) {
+ console.log(`\nAuditing: ${template.name}`);
+ const seedRecord = seedTemplates.get(template.name);
+ const result = await auditTemplate(template, seedRecord);
+ results.push(result);
+ console.log(
+ ` ${result.recommendation.toUpperCase()} ${result.terminalStatus ?? result.runStartError ?? result.createError ?? 'created'}`,
+ );
+ }
+
+ const jsonPath = join(OUTPUT_ROOT, 'template-live-audit.json');
+ const mdPath = join(OUTPUT_ROOT, 'template-live-audit.md');
+ writeFileSync(
+ jsonPath,
+ JSON.stringify({ apiBase: API_BASE, generatedAt: new Date().toISOString(), results }, null, 2),
+ );
+ writeFileSync(
+ mdPath,
+ renderTemplateAuditMarkdown({
+ apiBase: API_BASE,
+ outputRoot: OUTPUT_ROOT,
+ generatedAt: new Date().toISOString(),
+ results,
+ }),
+ );
+
+ console.log(`\nAudit complete.`);
+ console.log(`JSON: ${jsonPath}`);
+ console.log(`Markdown: ${mdPath}`);
+
+ const failingLiveRuns = results.filter(
+ (result) => result.classification === 'live-run' && result.terminalStatus !== 'COMPLETED',
+ );
+ if (failingLiveRuns.length > 0) {
+ process.exitCode = 1;
+ console.error(
+ `Live-run templates failed: ${failingLiveRuns.map((result) => result.templateName).join(', ')}`,
+ );
+ }
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/worker/src/components/core/__tests__/http-request.test.ts b/worker/src/components/core/__tests__/http-request.test.ts
index 3595d2be..c5a6f6d3 100644
--- a/worker/src/components/core/__tests__/http-request.test.ts
+++ b/worker/src/components/core/__tests__/http-request.test.ts
@@ -252,4 +252,33 @@ describe('HTTP Request Component', () => {
expect(e.message).toContain('timed out');
}
});
+
+ test('should return structured timeout error if failOnError is false', async () => {
+ const result = await definition.execute(
+ {
+ inputs: {
+ url: `${baseUrl}/timeout`,
+ },
+ params: {
+ method: 'GET',
+ timeout: 50,
+ contentType: 'application/json',
+ failOnError: false,
+ authType: 'none',
+ },
+ },
+ mockContext,
+ );
+
+ expect(result.status).toBe(0);
+ expect(result.statusText).toBe('Timeout');
+ expect(result.rawBody).toBe('');
+ expect(result.headers).toEqual({});
+ expect(result.data).toMatchObject({
+ error: {
+ type: 'timeout',
+ message: expect.stringContaining('timed out'),
+ },
+ });
+ });
});
diff --git a/worker/src/components/core/http-request.ts b/worker/src/components/core/http-request.ts
index d68f4d04..8c2f169e 100644
--- a/worker/src/components/core/http-request.ts
+++ b/worker/src/components/core/http-request.ts
@@ -301,9 +301,30 @@ const definition = defineComponent({
} catch (error: any) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
- throw new TimeoutError(`HTTP request timed out after ${timeout}ms`, timeout, {
- details: { url, method },
- });
+ const timeoutError = new TimeoutError(
+ `HTTP request timed out after ${timeout}ms`,
+ timeout,
+ {
+ details: { url, method },
+ },
+ );
+ if (!failOnError) {
+ context.logger.warn(`[HTTP] ${method} ${url} timed out after ${timeout}ms`);
+ return {
+ status: 0,
+ statusText: 'Timeout',
+ data: {
+ error: {
+ type: 'timeout',
+ message: timeoutError.message,
+ timeoutMs: timeout,
+ },
+ },
+ headers: {},
+ rawBody: '',
+ };
+ }
+ throw timeoutError;
}
// Wrap network errors appropriately
if (
@@ -313,6 +334,21 @@ const definition = defineComponent({
error.message?.includes('socket hang up') ||
error.name === 'FetchError'
) {
+ if (!failOnError) {
+ context.logger.warn(`[HTTP] ${method} ${url} failed: ${error.message}`);
+ return {
+ status: 0,
+ statusText: 'Network Error',
+ data: {
+ error: {
+ type: 'network',
+ message: error.message ?? 'Network error',
+ },
+ },
+ headers: {},
+ rawBody: '',
+ };
+ }
throw NetworkError.from(error);
}
throw error;
diff --git a/worker/src/components/index.ts b/worker/src/components/index.ts
index 302bbc2c..56ce0234 100644
--- a/worker/src/components/index.ts
+++ b/worker/src/components/index.ts
@@ -72,6 +72,8 @@ import './security/katana';
import './security/ffuf';
import './security/trivy';
import './security/semgrep';
+import './security/osv';
+import './security/nvd';
import './security/yara';
// GitHub components
diff --git a/worker/src/components/security/__tests__/httpx.test.ts b/worker/src/components/security/__tests__/httpx.test.ts
index cb1928cf..93254fdd 100644
--- a/worker/src/components/security/__tests__/httpx.test.ts
+++ b/worker/src/components/security/__tests__/httpx.test.ts
@@ -1,12 +1,30 @@
import { describe, expect, test, beforeAll, afterEach, vi } from 'bun:test';
import * as sdk from '@sentris/component-sdk';
import { componentRegistry } from '../../index';
-import { parseHttpxOutput } from '../httpx';
+import { buildHttpxArgs, parseHttpxOutput } from '../httpx';
import type { HttpxOutput, InputShape, OutputShape } from '../httpx';
const runHttpxTests = process.env.ENABLE_HTTPX_COMPONENT_TESTS === 'true';
const describeHttpx = runHttpxTests ? describe : describe.skip;
+describe('httpx argument builder', () => {
+ test('does not emit unsupported prefer-https flag', () => {
+ const args = buildHttpxArgs({
+ ports: undefined,
+ statusCodes: undefined,
+ threads: undefined,
+ path: undefined,
+ followRedirects: true,
+ tlsProbe: true,
+ preferHttps: true,
+ });
+
+ expect(args).toContain('-follow-redirects');
+ expect(args).toContain('-tls-probe');
+ expect(args).not.toContain('-prefer-https');
+ });
+});
+
describeHttpx('httpx component', () => {
beforeAll(async () => {
await import('../../index');
@@ -128,7 +146,7 @@ describeHttpx('httpx component', () => {
context,
)) as HttpxOutput;
- expect(result.results).toHaveLength(1);
+ expect(result.responses).toHaveLength(1);
expect(result.resultCount).toBe(1);
expect(result.rawOutput).toContain('https://example.com');
expect(result.options.followRedirects).toBe(false);
@@ -145,7 +163,6 @@ describeHttpx('httpx component', () => {
const params = component.inputs.parse({
targets: ['https://example.com'],
- followRedirects: true,
});
const raw = [
@@ -155,13 +172,56 @@ describeHttpx('httpx component', () => {
vi.spyOn(sdk, 'runComponentWithRunner').mockResolvedValue(raw);
- const result = await component.execute({ inputs: params, params: {} }, context);
+ const result = await component.execute(
+ { inputs: params, params: { followRedirects: true } },
+ context,
+ );
expect(result.results).toHaveLength(2);
expect(result.options.followRedirects).toBe(true);
expect(result.rawOutput).toContain('https://other.example');
});
+ test('parses a single httpx JSON object returned by stdout fallback', async () => {
+ const component = componentRegistry.get('sentris.httpx.scan');
+ if (!component) throw new Error('Component not registered');
+
+ const context = sdk.createExecutionContext({
+ runId: 'test-run',
+ componentRef: 'httpx-test',
+ });
+
+ const params = component.inputs.parse({
+ targets: ['https://example.com'],
+ });
+
+ vi.spyOn(sdk, 'runComponentWithRunner').mockResolvedValue({
+ url: 'https://example.com',
+ input: 'https://example.com',
+ host: 'example.com',
+ status_code: 200,
+ title: 'Example Domain',
+ scheme: 'https',
+ webserver: 'ECS',
+ tech: ['HTTP'],
+ });
+
+ const result = await component.execute(
+ { inputs: params, params: { followRedirects: true } },
+ context,
+ );
+
+ expect(result.responses).toHaveLength(1);
+ expect(result.resultCount).toBe(1);
+ expect(result.responses[0]).toMatchObject({
+ url: 'https://example.com',
+ statusCode: 200,
+ title: 'Example Domain',
+ technologies: ['HTTP'],
+ });
+ expect(result.rawOutput).toContain('"url":"https://example.com"');
+ });
+
test('skips execution when no targets are provided', async () => {
const component = componentRegistry.get('sentris.httpx.scan');
if (!component) throw new Error('Component not registered');
diff --git a/worker/src/components/security/__tests__/katana.test.ts b/worker/src/components/security/__tests__/katana.test.ts
new file mode 100644
index 00000000..12dcb090
--- /dev/null
+++ b/worker/src/components/security/__tests__/katana.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, test } from 'bun:test';
+import { buildKatanaArgs, mapKatanaScope } from '../katana';
+
+describe('katana argument builder', () => {
+ test('uses current JSONL output flag and maps Sentris scope values', () => {
+ const args = buildKatanaArgs({
+ depth: 2,
+ scope: 'strict',
+ timeout: 300,
+ headless: false,
+ customFlags: [],
+ });
+
+ expect(args).toContain('-jsonl');
+ expect(args).not.toContain('-json');
+ expect(args).toContain('-field-scope');
+ expect(args[args.indexOf('-field-scope') + 1]).toBe('fqdn');
+ expect(mapKatanaScope('fuzzy')).toBe('rdn');
+ expect(mapKatanaScope('subs')).toBe('dn');
+ });
+
+ test('appends browser and custom flags after stable defaults', () => {
+ const args = buildKatanaArgs({
+ depth: 1,
+ scope: 'subs',
+ timeout: 60,
+ headless: true,
+ customFlags: ['-known-files', 'all'],
+ });
+
+ expect(args).toContain('-headless');
+ expect(args).toContain('-timeout');
+ expect(args[args.indexOf('-timeout') + 1]).toBe('60');
+ expect(args.slice(-2)).toEqual(['-known-files', 'all']);
+ });
+});
diff --git a/worker/src/components/security/__tests__/nuclei.test.ts b/worker/src/components/security/__tests__/nuclei.test.ts
index c423c1a1..335e0a2a 100644
--- a/worker/src/components/security/__tests__/nuclei.test.ts
+++ b/worker/src/components/security/__tests__/nuclei.test.ts
@@ -1,6 +1,6 @@
import { describe, test, expect, beforeEach, mock } from 'bun:test';
import { componentRegistry } from '@sentris/component-sdk';
-import type { NucleiInput, NucleiOutput } from '../nuclei';
+import { buildNucleiDockerCommand, type NucleiInput, type NucleiOutput } from '../nuclei';
// Import to trigger registration
import '../nuclei';
@@ -111,7 +111,7 @@ describe('Nuclei Component', () => {
expect(parsedParams.retries).toBe(1);
expect(parsedParams.includeRaw).toBe(false);
expect(parsedParams.followRedirects).toBe(false);
- expect(parsedParams.updateTemplates).toBe(false);
+ expect(parsedParams.updateTemplates).toBe(true);
expect(parsedParams.disableHttpx).toBe(true);
});
@@ -216,6 +216,20 @@ describe('Nuclei Component', () => {
});
});
+describe('Nuclei Docker launcher', () => {
+ test('updates built-in templates before forwarding scanner arguments', () => {
+ const scanArgs = ['-jsonl', '-l', '/inputs/targets.txt', '-t', 'http/exposures/'];
+
+ const command = buildNucleiDockerCommand(scanArgs, true);
+
+ expect(command[0]).toBe('-lc');
+ expect(command[1]).toContain('nuclei -update-templates');
+ expect(command[1]).toContain('exec nuclei "$@"');
+ expect(command.slice(2)).toEqual(['nuclei', ...scanArgs]);
+ expect(command).not.toContain('-update-templates');
+ });
+});
+
describe('Nuclei Helper Functions', () => {
describe('validateNucleiTemplate', () => {
// Import the helper (we'll need to export it from nuclei.ts)
@@ -422,8 +436,9 @@ describe('Nuclei Integration', () => {
const component = componentRegistry.get('sentris.nuclei.scan')!;
expect(component!.runner.kind).toBe('docker');
if (component!.runner.kind === 'docker') {
- expect(component!.runner.image).toBe('ghcr.io/zebbern/nuclei:latest');
- expect(component!.runner.entrypoint).toBe('nuclei');
+ expect(component!.runner.image).toBe('projectdiscovery/nuclei:latest');
+ expect(component!.runner.entrypoint).toBe('sh');
+ expect(component!.runner.memoryLimit).toBe('1g');
}
});
diff --git a/worker/src/components/security/__tests__/nvd.test.ts b/worker/src/components/security/__tests__/nvd.test.ts
new file mode 100644
index 00000000..c892d0d4
--- /dev/null
+++ b/worker/src/components/security/__tests__/nvd.test.ts
@@ -0,0 +1,173 @@
+import { afterEach, describe, expect, it, vi } from 'bun:test';
+import {
+ componentRegistry,
+ createExecutionContext,
+ type ExecutionContext,
+} from '@sentris/component-sdk';
+import { buildNvdCveUrl, type NvdCveInput, type NvdCveOutput } from '../nvd';
+import '../nvd';
+
+const sampleNvdResponse = {
+ resultsPerPage: 1,
+ startIndex: 0,
+ totalResults: 1,
+ vulnerabilities: [
+ {
+ cve: {
+ id: 'CVE-2024-3094',
+ published: '2024-03-29T17:15:07.547',
+ lastModified: '2025-02-01T15:15:00.000',
+ vulnStatus: 'Analyzed',
+ descriptions: [
+ {
+ lang: 'en',
+ value: 'Sample backdoor vulnerability description.',
+ },
+ ],
+ },
+ },
+ ],
+};
+
+describe('NVD CVE query component', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('registers with component metadata', () => {
+ const component = componentRegistry.get('sentris.nvd.cve.query');
+
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('NVD CVE Query');
+ expect(component?.category).toBe('security');
+ });
+
+ it('builds cveIds queries and excludes rejected CVEs by default', () => {
+ const url = buildNvdCveUrl({
+ cveIds: [' cve-2024-3094 ', 'CVE-2024-3094', 'CVE-2021-44228'],
+ keywordSearch: '',
+ resultsPerPage: 50,
+ includeRejected: false,
+ });
+
+ expect(url).toContain('https://services.nvd.nist.gov/rest/json/cves/2.0?');
+ expect(url).toContain('cveIds=CVE-2024-3094%2CCVE-2021-44228');
+ expect(url).toContain('resultsPerPage=50');
+ expect(url).toContain('startIndex=0');
+ expect(url).toContain('noRejected');
+ expect(url).not.toContain('cveId=');
+ });
+
+ it('builds keywordSearch queries when no CVE IDs are supplied', () => {
+ const url = buildNvdCveUrl({
+ cveIds: [],
+ keywordSearch: 'apache airflow',
+ resultsPerPage: 20,
+ includeRejected: true,
+ });
+
+ expect(url).toContain('keywordSearch=apache+airflow');
+ expect(url).toContain('resultsPerPage=20');
+ expect(url).not.toContain('noRejected');
+ });
+
+ it('queries NVD, forwards apiKey as a header, and returns source health metadata', async () => {
+ const component = componentRegistry.get('sentris.nvd.cve.query');
+ if (!component) throw new Error('NVD CVE component was not registered');
+
+ const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
+ expect(init?.method).toBe('GET');
+ expect((init?.headers as Record).apiKey).toBe('nvd-test-key');
+ return new Response(JSON.stringify(sampleNvdResponse), {
+ status: 200,
+ statusText: 'OK',
+ headers: { 'Content-Type': 'application/json' },
+ });
+ });
+
+ const context = createExecutionContext({
+ runId: 'test-run',
+ componentRef: 'nvd-test',
+ });
+ context.http.fetch = fetchMock as unknown as ExecutionContext['http']['fetch'];
+
+ const result = await component.execute(
+ {
+ inputs: {
+ cveIds: ['CVE-2024-3094'],
+ keywordSearch: '',
+ apiKey: 'nvd-test-key',
+ },
+ params: {
+ resultsPerPage: 20,
+ includeRejected: false,
+ timeoutMs: 30000,
+ failOnUnavailable: false,
+ },
+ },
+ context,
+ );
+
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ expect(result).toMatchObject({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ warnings: [],
+ totalResults: 1,
+ returnedResults: 1,
+ });
+ expect(result.vulnerabilities).toHaveLength(1);
+ expect(result.dataSource).toMatchObject({
+ name: 'nvd',
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ });
+ expect(result.data).toEqual(sampleNvdResponse);
+ });
+
+ it('returns a non-fatal timeout result when failOnUnavailable is false', async () => {
+ const component = componentRegistry.get('sentris.nvd.cve.query');
+ if (!component) throw new Error('NVD CVE component was not registered');
+
+ const timeoutError = new DOMException('The operation was aborted.', 'AbortError');
+ const fetchMock = vi.fn(async () => {
+ throw timeoutError;
+ });
+
+ const context = createExecutionContext({
+ runId: 'test-run',
+ componentRef: 'nvd-timeout-test',
+ });
+ context.http.fetch = fetchMock as unknown as ExecutionContext['http']['fetch'];
+
+ const result = await component.execute(
+ {
+ inputs: {
+ cveIds: [],
+ keywordSearch: 'nginx',
+ apiKey: '',
+ },
+ params: {
+ resultsPerPage: 5,
+ includeRejected: false,
+ timeoutMs: 1000,
+ failOnUnavailable: false,
+ },
+ },
+ context,
+ );
+
+ expect(result).toMatchObject({
+ ok: false,
+ status: 0,
+ statusText: 'Timeout',
+ vulnerabilities: [],
+ totalResults: 0,
+ returnedResults: 0,
+ });
+ expect(result.warnings).toEqual(['NVD CVE query unavailable: Timeout']);
+ expect(result.data).toEqual({ error: 'Timeout' });
+ });
+});
diff --git a/worker/src/components/security/__tests__/osv.test.ts b/worker/src/components/security/__tests__/osv.test.ts
new file mode 100644
index 00000000..7b3cca1f
--- /dev/null
+++ b/worker/src/components/security/__tests__/osv.test.ts
@@ -0,0 +1,160 @@
+import { afterEach, describe, expect, it, vi } from 'bun:test';
+import {
+ componentRegistry,
+ createExecutionContext,
+ type ExecutionContext,
+} from '@sentris/component-sdk';
+import {
+ extractFixedVersions,
+ inferOsvSeverity,
+ parsePackageSpec,
+ type OsvInput,
+ type OsvOutput,
+} from '../osv';
+import '../osv';
+
+const sampleAdvisory = {
+ id: 'GHSA-test',
+ summary: 'Prototype Pollution in test package',
+ aliases: ['CVE-2026-12345'],
+ modified: '2026-01-01T00:00:00Z',
+ published: '2025-12-01T00:00:00Z',
+ database_specific: {
+ severity: 'HIGH',
+ },
+ references: [
+ {
+ type: 'ADVISORY',
+ url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-12345',
+ },
+ ],
+ affected: [
+ {
+ package: {
+ ecosystem: 'npm',
+ name: 'lodash',
+ },
+ ranges: [
+ {
+ type: 'SEMVER',
+ events: [{ introduced: '0' }, { fixed: '4.17.21' }],
+ },
+ ],
+ },
+ ],
+};
+
+describe('OSV dependency query component', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('registers with component metadata', () => {
+ const component = componentRegistry.get('sentris.osv.query');
+
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('OSV Dependency Advisory Query');
+ expect(component?.category).toBe('security');
+ });
+
+ it('parses npm package specs with scoped package support', () => {
+ expect(parsePackageSpec('lodash@4.17.20', 'npm')).toEqual({
+ spec: 'lodash@4.17.20',
+ name: 'lodash',
+ version: '4.17.20',
+ ecosystem: 'npm',
+ });
+ expect(parsePackageSpec('@scope/pkg@1.2.3', 'npm')).toEqual({
+ spec: '@scope/pkg@1.2.3',
+ name: '@scope/pkg',
+ version: '1.2.3',
+ ecosystem: 'npm',
+ });
+ expect(parsePackageSpec('axios', 'npm')).toEqual({
+ spec: 'axios',
+ name: 'axios',
+ version: null,
+ ecosystem: 'npm',
+ });
+ expect(parsePackageSpec('', 'npm')).toBeNull();
+ });
+
+ it('infers severity and fixed versions from hydrated OSV advisories', () => {
+ expect(inferOsvSeverity(sampleAdvisory)).toBe('high');
+ expect(extractFixedVersions(sampleAdvisory)).toEqual(['4.17.21']);
+ });
+
+ it('queries OSV, hydrates advisories, and emits analytics-ready results', async () => {
+ const component = componentRegistry.get('sentris.osv.query');
+ if (!component) throw new Error('OSV component was not registered');
+
+ const fetchMock = vi.fn(async (url: string | URL | Request): Promise => {
+ const text = String(url);
+ if (text.endsWith('/v1/querybatch')) {
+ return new Response(
+ JSON.stringify({
+ results: [{ vulns: [{ id: 'GHSA-test', modified: '2026-01-01T00:00:00Z' }] }],
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ );
+ }
+ if (text.endsWith('/v1/vulns/GHSA-test')) {
+ return new Response(JSON.stringify(sampleAdvisory), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+ return new Response('not found', { status: 404 });
+ });
+
+ const context = createExecutionContext({
+ runId: 'test-run',
+ componentRef: 'osv-test',
+ });
+ context.http.fetch = fetchMock as unknown as ExecutionContext['http']['fetch'];
+
+ const result = await component.execute(
+ {
+ inputs: {
+ packageSpecs: ['lodash@4.17.20'],
+ },
+ params: {
+ ecosystem: 'npm',
+ severityFloor: 'medium',
+ hydrateAdvisories: true,
+ maxAdvisoriesPerPackage: 50,
+ includeUnknownSeverity: true,
+ },
+ },
+ context,
+ );
+
+ expect(fetchMock).toHaveBeenCalledTimes(2);
+ expect(result.summary).toEqual({
+ packagesChecked: 1,
+ vulnerablePackages: 1,
+ findings: 1,
+ maliciousPackageRecords: 0,
+ countsBySeverity: { high: 1 },
+ });
+ expect(result.findings[0]).toMatchObject({
+ packageSpec: 'lodash@4.17.20',
+ packageName: 'lodash',
+ version: '4.17.20',
+ id: 'GHSA-test',
+ cves: ['CVE-2026-12345'],
+ fixedVersions: ['4.17.21'],
+ severity: 'high',
+ summary: 'Prototype Pollution in test package',
+ });
+ expect(result.results[0]).toMatchObject({
+ scanner: 'osv',
+ severity: 'high',
+ asset_key: 'lodash@4.17.20',
+ vulnerability_id: 'GHSA-test',
+ package_name: 'lodash',
+ installed_version: '4.17.20',
+ fixed_versions: ['4.17.21'],
+ });
+ });
+});
diff --git a/worker/src/components/security/httpx.ts b/worker/src/components/security/httpx.ts
index b26c0189..5dd8f02e 100644
--- a/worker/src/components/security/httpx.ts
+++ b/worker/src/components/security/httpx.ts
@@ -146,6 +146,15 @@ const findingSchema = z.object({
});
type Finding = z.infer;
+interface HttpxArgOptions {
+ ports?: string;
+ statusCodes?: string;
+ threads?: number;
+ path?: string;
+ followRedirects: boolean;
+ tlsProbe: boolean;
+ preferHttps: boolean;
+}
const outputSchema = outputs({
responses: port(z.array(findingSchema), {
@@ -188,12 +197,14 @@ const outputSchema = outputs({
}),
});
-const httpxRunnerOutputSchema = z.object({
- results: z.array(z.unknown()).optional().default([]),
- raw: z.string().optional().default(''),
- stderr: z.string().optional().default(''),
- exitCode: z.number().optional().default(0),
-});
+const httpxRunnerOutputSchema = z
+ .object({
+ results: z.array(z.unknown()).optional().default([]),
+ raw: z.string().optional().default(''),
+ stderr: z.string().optional().default(''),
+ exitCode: z.number().optional().default(0),
+ })
+ .strict();
const dockerTimeoutSeconds = (() => {
const raw = process.env.HTTPX_TIMEOUT_SECONDS;
@@ -322,29 +333,7 @@ const definition = defineComponent({
'targets.txt': targets.join('\n'),
});
- const httpxArgs: string[] = ['-json', '-silent', '-l', '/inputs/targets.txt', '-stream'];
-
- if (runnerParams.ports) {
- httpxArgs.push('-ports', runnerParams.ports);
- }
- if (runnerParams.statusCodes) {
- httpxArgs.push('-status-code', runnerParams.statusCodes);
- }
- if (typeof runnerParams.threads === 'number') {
- httpxArgs.push('-threads', String(runnerParams.threads));
- }
- if (runnerParams.path) {
- httpxArgs.push('-path', runnerParams.path);
- }
- if (runnerParams.followRedirects) {
- httpxArgs.push('-follow-redirects');
- }
- if (runnerParams.tlsProbe) {
- httpxArgs.push('-tls-probe');
- }
- if (runnerParams.preferHttps) {
- httpxArgs.push('-prefer-https');
- }
+ const httpxArgs = buildHttpxArgs(runnerParams);
const runnerConfig = {
...definition.runner,
@@ -542,6 +531,31 @@ function parseHttpxOutput(raw: string): Finding[] {
return findings;
}
+export function buildHttpxArgs(options: HttpxArgOptions): string[] {
+ const httpxArgs: string[] = ['-json', '-silent', '-l', '/inputs/targets.txt', '-stream'];
+
+ if (options.ports) {
+ httpxArgs.push('-ports', options.ports);
+ }
+ if (options.statusCodes) {
+ httpxArgs.push('-status-code', options.statusCodes);
+ }
+ if (typeof options.threads === 'number') {
+ httpxArgs.push('-threads', String(options.threads));
+ }
+ if (options.path) {
+ httpxArgs.push('-path', options.path);
+ }
+ if (options.followRedirects) {
+ httpxArgs.push('-follow-redirects');
+ }
+ if (options.tlsProbe) {
+ httpxArgs.push('-tls-probe');
+ }
+
+ return httpxArgs;
+}
+
function normaliseNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
diff --git a/worker/src/components/security/katana.ts b/worker/src/components/security/katana.ts
index f5c4d140..66794e72 100644
--- a/worker/src/components/security/katana.ts
+++ b/worker/src/components/security/katana.ts
@@ -144,6 +144,55 @@ const splitCliArgs = (input: string): string[] => {
return args;
};
+type KatanaScope = z.infer['scope'];
+
+export function mapKatanaScope(scope: KatanaScope): 'fqdn' | 'rdn' | 'dn' {
+ switch (scope) {
+ case 'fuzzy':
+ return 'rdn';
+ case 'subs':
+ return 'dn';
+ case 'strict':
+ default:
+ return 'fqdn';
+ }
+}
+
+export function buildKatanaArgs(options: {
+ depth: number;
+ scope: KatanaScope;
+ timeout?: number;
+ headless: boolean;
+ customFlags: string[];
+}): string[] {
+ const args: string[] = [
+ '-list',
+ '/inputs/targets.txt',
+ '-jsonl',
+ '-silent',
+ '-depth',
+ String(options.depth),
+ '-field-scope',
+ mapKatanaScope(options.scope),
+ ];
+
+ if (options.headless) {
+ args.push('-headless');
+ }
+
+ if (options.timeout) {
+ args.push('-timeout', String(options.timeout));
+ }
+
+ for (const flag of options.customFlags) {
+ if (flag.length > 0) {
+ args.push(flag);
+ }
+ }
+
+ return args;
+}
+
const katanaRetryPolicy: ComponentRetryPolicy = {
maxAttempts: 2,
initialIntervalSeconds: 5,
@@ -152,11 +201,13 @@ const katanaRetryPolicy: ComponentRetryPolicy = {
nonRetryableErrorTypes: ['ContainerError', 'ValidationError', 'ConfigurationError'],
};
-const runnerOutputSchema = z.object({
- stdout: z.string().optional().default(''),
- stderr: z.string().optional().default(''),
- exitCode: z.number().optional().default(0),
-});
+const runnerOutputSchema = z
+ .object({
+ stdout: z.string().optional().default(''),
+ stderr: z.string().optional().default(''),
+ exitCode: z.number().optional().default(0),
+ })
+ .strict();
const definition = defineComponent({
id: 'sentris.katana.run',
@@ -195,7 +246,7 @@ const definition = defineComponent({
isLatest: true,
deprecated: false,
example:
- '`katana -u https://example.com -depth 3 -json` - Crawl example.com to depth 3 and output JSON.',
+ '`katana -u https://example.com -depth 3 -jsonl` - Crawl example.com to depth 3 and output JSONL.',
examples: [
'Discover hidden endpoints and API routes before vulnerability scanning.',
'Map application attack surface by crawling JavaScript files and forms.',
@@ -262,32 +313,13 @@ const definition = defineComponent({
});
context.logger.info(`[Katana] Created isolated volume: ${volume.getVolumeName()}`);
- // Build Katana CLI args
- const args: string[] = [
- '-list',
- '/inputs/targets.txt',
- '-json',
- '-silent',
- '-depth',
- String(parsedParams.depth),
- '-field-scope',
- parsedParams.scope,
- ];
-
- if (parsedParams.headless) {
- args.push('-headless');
- }
-
- if (parsedParams.timeout) {
- args.push('-timeout', String(parsedParams.timeout));
- }
-
- // Append custom flags last
- for (const flag of customFlagArgs) {
- if (flag.length > 0) {
- args.push(flag);
- }
- }
+ const args = buildKatanaArgs({
+ depth: parsedParams.depth,
+ scope: parsedParams.scope,
+ timeout: parsedParams.timeout,
+ headless: parsedParams.headless,
+ customFlags: customFlagArgs,
+ });
const runnerConfig: DockerRunnerConfig = {
kind: 'docker',
@@ -316,7 +348,7 @@ const definition = defineComponent({
if (parsed.success) {
rawOutput = parsed.data.stdout ?? '';
} else {
- rawOutput = '';
+ rawOutput = JSON.stringify(result);
}
} else {
rawOutput = '';
diff --git a/worker/src/components/security/nuclei.ts b/worker/src/components/security/nuclei.ts
index 18e9fa01..b2fb7fe1 100644
--- a/worker/src/components/security/nuclei.ts
+++ b/worker/src/components/security/nuclei.ts
@@ -143,7 +143,7 @@ const parameterSchema = parameters({
},
),
updateTemplates: param(
- z.boolean().default(false).describe('Update built-in templates before scanning'),
+ z.boolean().default(true).describe('Update built-in templates before scanning'),
{
label: 'Update Templates',
editor: 'boolean',
@@ -262,6 +262,14 @@ const nucleiRetryPolicy: ComponentRetryPolicy = {
nonRetryableErrorTypes: ['ContainerError', 'ValidationError', 'ConfigurationError'],
};
+export function buildNucleiDockerCommand(scanArgs: string[], updateTemplates: boolean): string[] {
+ const launcher = updateTemplates
+ ? 'nuclei -update-templates -silent || nuclei -update-templates || exit $?; exec nuclei "$@"'
+ : 'exec nuclei "$@"';
+
+ return ['-lc', launcher, 'nuclei', ...scanArgs];
+}
+
const definition = defineComponent({
id: 'sentris.nuclei.scan',
label: 'Nuclei Vulnerability Scanner',
@@ -269,19 +277,14 @@ const definition = defineComponent({
retryPolicy: nucleiRetryPolicy,
runner: {
kind: 'docker',
- // Custom image with pre-installed nuclei-templates baked in at build time.
- // The upstream projectdiscovery/nuclei:latest ships WITHOUT templates,
- // so severity-filtered scans find 0 templates and produce no results.
- // Build: docker build -t ghcr.io/zebbern/nuclei:latest docker/nuclei/
- image: 'ghcr.io/zebbern/nuclei:latest',
- entrypoint: 'nuclei',
+ image: 'projectdiscovery/nuclei:latest',
+ entrypoint: 'sh',
network: 'bridge',
timeoutSeconds: dockerTimeoutSeconds,
- // Direct binary execution (distroless image has no shell)
- // PTY compatibility achieved via -stream flag (prevents buffering)
+ memoryLimit: '1g',
command: [],
env: {
- HOME: '/home/nonroot', // Custom image runs as nonroot user
+ HOME: '/root',
},
},
inputs: inputSchema,
@@ -351,7 +354,6 @@ const definition = defineComponent({
'-duc', // Disable update check (templates pre-installed in image)
'-jsonl', // JSONL output format (nuclei v3.6.0+)
'-stream', // Stream mode: prevents buffering, required for PTY compatibility
- '-verbose', // Show findings in terminal (overrides silent mode)
'-l',
'/inputs/targets.txt', // Targets file
];
@@ -367,10 +369,6 @@ const definition = defineComponent({
args.push('-timeout', parsedParams.timeout.toString());
args.push('-retries', parsedParams.retries.toString());
- if (parsedParams.updateTemplates) {
- args.push('-update-templates');
- }
-
if (parsedParams.followRedirects) {
args.push('-follow-redirects');
}
@@ -500,14 +498,15 @@ const definition = defineComponent({
entrypoint: baseRunner.entrypoint,
network: baseRunner.network,
timeoutSeconds: baseRunner.timeoutSeconds,
+ memoryLimit: baseRunner.memoryLimit,
+ cpuLimit: baseRunner.cpuLimit,
+ pidsLimit: baseRunner.pidsLimit,
env: baseRunner.env,
- // ✅ Preserve shell wrapper + append TypeScript-built args
- command: [...(baseRunner.command ?? []), ...args],
- volumes: [
- volume.getVolumeConfig('/inputs', true),
- // ✅ Templates are pre-installed in ghcr.io/zebbern/nuclei:latest
- // No need for persistent volume since templates are baked into the image
- ],
+ command: buildNucleiDockerCommand(
+ [...(baseRunner.command ?? []), ...args],
+ parsedParams.updateTemplates,
+ ),
+ volumes: [volume.getVolumeConfig('/inputs', true)],
};
// ===== Execute nuclei =====
diff --git a/worker/src/components/security/nvd.ts b/worker/src/components/security/nvd.ts
new file mode 100644
index 00000000..a49153a9
--- /dev/null
+++ b/worker/src/components/security/nvd.ts
@@ -0,0 +1,455 @@
+import { z } from 'zod';
+import {
+ ComponentRetryPolicy,
+ componentRegistry,
+ defineComponent,
+ fromHttpResponse,
+ inputs,
+ outputs,
+ parameters,
+ param,
+ port,
+ ValidationError,
+ type ExecutionContext,
+} from '@sentris/component-sdk';
+
+const NVD_CVE_API_URL = 'https://services.nvd.nist.gov/rest/json/cves/2.0';
+const NVD_DOCS_URL = 'https://nvd.nist.gov/developers/vulnerabilities';
+
+const cveIdPattern = /^CVE-\d{4}-\d{4,}$/i;
+
+const cveIdsInputSchema = z.preprocess((value) => {
+ if (Array.isArray(value)) return value;
+ if (typeof value === 'string') {
+ return value
+ .split(/[,\s]+/)
+ .map((item) => item.trim())
+ .filter(Boolean);
+ }
+ return [];
+}, z.array(z.string()).default([]));
+
+const inputSchema = inputs({
+ cveIds: port(cveIdsInputSchema, {
+ label: 'CVE IDs',
+ description:
+ 'One or more CVE identifiers. When supplied, these take precedence over keyword search.',
+ connectionType: { kind: 'primitive', name: 'text' },
+ }),
+ keywordSearch: port(z.string().optional().default(''), {
+ label: 'Keyword Search',
+ description: 'NVD keyword search used when no CVE IDs are supplied.',
+ connectionType: { kind: 'primitive', name: 'text' },
+ }),
+ apiKey: port(z.string().optional().default(''), {
+ label: 'NVD API Key',
+ description: 'Optional NVD API key. Sent as the apiKey request header.',
+ connectionType: { kind: 'primitive', name: 'secret' },
+ }),
+});
+
+const parameterSchema = parameters({
+ resultsPerPage: param(
+ z
+ .number()
+ .int()
+ .min(1)
+ .max(2000)
+ .default(20)
+ .describe('Maximum CVE records to return from NVD.'),
+ {
+ label: 'Results Per Page',
+ editor: 'number',
+ min: 1,
+ max: 2000,
+ },
+ ),
+ includeRejected: param(
+ z.boolean().default(false).describe('Include CVE records marked rejected by NVD.'),
+ {
+ label: 'Include Rejected CVEs',
+ editor: 'boolean',
+ },
+ ),
+ timeoutMs: param(
+ z
+ .number()
+ .int()
+ .min(1000)
+ .max(120000)
+ .default(30000)
+ .describe('Request timeout in milliseconds.'),
+ {
+ label: 'Timeout (ms)',
+ editor: 'number',
+ min: 1000,
+ max: 120000,
+ },
+ ),
+ failOnUnavailable: param(
+ z
+ .boolean()
+ .default(false)
+ .describe('Throw when NVD is unavailable instead of returning warnings.'),
+ {
+ label: 'Fail On Unavailable',
+ editor: 'boolean',
+ },
+ ),
+});
+
+const dataSourceSchema = z.object({
+ name: z.literal('nvd'),
+ ok: z.boolean(),
+ status: z.number(),
+ statusText: z.string(),
+ url: z.string(),
+ docsUrl: z.string(),
+});
+
+const querySchema = z.object({
+ cveIds: z.array(z.string()),
+ keywordSearch: z.string().nullable(),
+ resultsPerPage: z.number(),
+ includeRejected: z.boolean(),
+});
+
+const summarySchema = z.object({
+ query: querySchema,
+ ok: z.boolean(),
+ status: z.number(),
+ statusText: z.string(),
+ totalResults: z.number(),
+ returnedResults: z.number(),
+ warnings: z.array(z.string()),
+});
+
+const outputSchema = outputs({
+ ok: port(z.boolean(), {
+ label: 'OK',
+ description: 'Whether NVD returned a successful HTTP response and valid JSON.',
+ }),
+ status: port(z.number(), {
+ label: 'HTTP Status',
+ description: 'NVD HTTP status code, or 0 for network/timeout failures.',
+ }),
+ statusText: port(z.string(), {
+ label: 'HTTP Status Text',
+ description: 'HTTP status text or normalized network failure reason.',
+ }),
+ url: port(z.string(), {
+ label: 'Request URL',
+ description: 'The NVD CVE API URL requested by this component.',
+ }),
+ dataSource: port(dataSourceSchema, {
+ label: 'Data Source',
+ description: 'NVD source health metadata for downstream reports.',
+ connectionType: { kind: 'primitive', name: 'json' },
+ }),
+ data: port(z.unknown(), {
+ label: 'Raw NVD Data',
+ description: 'Raw NVD CVE API response, or an error object when unavailable.',
+ allowAny: true,
+ reason: 'NVD response fields evolve over time and include nested CVE metadata.',
+ connectionType: { kind: 'primitive', name: 'json' },
+ }),
+ vulnerabilities: port(z.array(z.unknown()), {
+ label: 'Vulnerabilities',
+ description: 'NVD vulnerability records from the response body.',
+ allowAny: true,
+ reason: 'NVD CVE records contain a large schema that may evolve over time.',
+ connectionType: { kind: 'list', element: { kind: 'primitive', name: 'json' } },
+ }),
+ totalResults: port(z.number(), {
+ label: 'Total Results',
+ description: 'NVD totalResults value, or 0 when unavailable.',
+ }),
+ returnedResults: port(z.number(), {
+ label: 'Returned Results',
+ description: 'Number of vulnerability records returned in this response.',
+ }),
+ warnings: port(z.array(z.string()), {
+ label: 'Warnings',
+ description: 'Non-fatal availability or parsing warnings.',
+ }),
+ summary: port(summarySchema, {
+ label: 'Summary',
+ description: 'Query, source health, and result count summary.',
+ connectionType: { kind: 'primitive', name: 'json' },
+ }),
+});
+
+interface NvdCveUrlOptions {
+ cveIds: string[];
+ keywordSearch?: string | null;
+ resultsPerPage: number;
+ includeRejected: boolean;
+}
+
+const nvdRetryPolicy: ComponentRetryPolicy = {
+ maxAttempts: 2,
+ initialIntervalSeconds: 2,
+ maximumIntervalSeconds: 15,
+ backoffCoefficient: 2.0,
+ nonRetryableErrorTypes: ['ValidationError', 'AuthenticationError', 'ConfigurationError'],
+};
+
+function normalizeCveIds(cveIds: string[]): string[] {
+ const normalized = new Set();
+ for (const item of cveIds) {
+ const cveId = item.trim().toUpperCase();
+ if (cveIdPattern.test(cveId)) normalized.add(cveId);
+ }
+ return Array.from(normalized).slice(0, 100);
+}
+
+export function buildNvdCveUrl(options: NvdCveUrlOptions): string {
+ const cveIds = normalizeCveIds(options.cveIds);
+ const keywordSearch = String(options.keywordSearch ?? '').trim();
+ const url = new URL(NVD_CVE_API_URL);
+
+ if (cveIds.length > 0) {
+ url.searchParams.set('cveIds', cveIds.join(','));
+ } else if (keywordSearch.length > 0) {
+ url.searchParams.set('keywordSearch', keywordSearch);
+ }
+
+ url.searchParams.set('resultsPerPage', String(options.resultsPerPage));
+ url.searchParams.set('startIndex', '0');
+
+ const requestUrl = url.toString();
+ return options.includeRejected ? requestUrl : `${requestUrl}&noRejected`;
+}
+
+function classifyFetchError(error: unknown): string {
+ const name = typeof error === 'object' && error ? String((error as { name?: unknown }).name) : '';
+ if (name === 'AbortError') return 'Timeout';
+
+ const message = error instanceof Error ? error.message : String(error);
+ if (/timeout|aborted/i.test(message)) return 'Timeout';
+ return 'Network Error';
+}
+
+function fallbackResult({
+ url,
+ query,
+ status,
+ statusText,
+ error,
+}: {
+ url: string;
+ query: z.infer;
+ status: number;
+ statusText: string;
+ error?: string;
+}) {
+ const warnings = [`NVD CVE query unavailable: ${statusText || status || 'unknown error'}`];
+ const dataSource = {
+ name: 'nvd' as const,
+ ok: false,
+ status,
+ statusText,
+ url,
+ docsUrl: NVD_DOCS_URL,
+ };
+
+ return {
+ ok: false,
+ status,
+ statusText,
+ url,
+ dataSource,
+ data: { error: error || statusText },
+ vulnerabilities: [],
+ totalResults: 0,
+ returnedResults: 0,
+ warnings,
+ summary: {
+ query,
+ ok: false,
+ status,
+ statusText,
+ totalResults: 0,
+ returnedResults: 0,
+ warnings,
+ },
+ };
+}
+
+async function fetchNvdJson(
+ context: Pick,
+ url: string,
+ headers: Record,
+ timeoutMs: number,
+): Promise {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
+ try {
+ return await context.http.fetch(url, {
+ method: 'GET',
+ headers,
+ signal: controller.signal,
+ });
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+const definition = defineComponent({
+ id: 'sentris.nvd.cve.query',
+ label: 'NVD CVE Query',
+ category: 'security',
+ runner: { kind: 'inline' },
+ retryPolicy: nvdRetryPolicy,
+ inputs: inputSchema,
+ outputs: outputSchema,
+ parameters: parameterSchema,
+ docs: 'Query the NVD CVE API by CVE ID or keyword and return raw data with normalized source health metadata.',
+ toolProvider: {
+ kind: 'component',
+ name: 'nvd_cve_query',
+ description: 'CVE metadata lookup using the NIST National Vulnerability Database.',
+ },
+ ui: {
+ slug: 'nvd-cve-query',
+ version: '1.0.0',
+ type: 'scan',
+ category: 'security',
+ description:
+ 'Look up CVE metadata in NVD by CVE ID or keyword with timeout-safe source status output.',
+ documentationUrl: NVD_DOCS_URL,
+ icon: 'ShieldAlert',
+ author: {
+ name: 'SentrisAI',
+ type: 'sentris',
+ },
+ isLatest: true,
+ deprecated: false,
+ examples: [
+ 'Fetch a known CVE such as CVE-2024-3094.',
+ 'Search for candidate CVEs from a detected service keyword such as nginx.',
+ ],
+ },
+ async execute({ inputs, params }, context) {
+ const parsedInputs = inputSchema.parse(inputs);
+ const parsedParams = parameterSchema.parse(params);
+ const cveIds = normalizeCveIds(parsedInputs.cveIds);
+ const keywordSearch = parsedInputs.keywordSearch.trim();
+
+ if (cveIds.length === 0 && keywordSearch.length === 0) {
+ throw new ValidationError('Provide at least one CVE ID or a keyword search value', {
+ fieldErrors: {
+ cveIds: ['Provide a CVE ID or keyword search value.'],
+ keywordSearch: ['Provide a CVE ID or keyword search value.'],
+ },
+ });
+ }
+
+ const query = {
+ cveIds,
+ keywordSearch: cveIds.length === 0 ? keywordSearch : null,
+ resultsPerPage: parsedParams.resultsPerPage,
+ includeRejected: parsedParams.includeRejected,
+ };
+ const url = buildNvdCveUrl(query);
+ const headers: Record = { Accept: 'application/json' };
+ const apiKey = parsedInputs.apiKey.trim();
+ if (apiKey.length > 0) headers.apiKey = apiKey;
+
+ context.logger.info(
+ `[NVD] Querying CVE API for ${cveIds.length > 0 ? cveIds.join(', ') : keywordSearch}`,
+ );
+ context.emitProgress({
+ message: `Querying NVD CVE API for ${cveIds.length > 0 ? cveIds.join(', ') : keywordSearch}`,
+ level: 'info',
+ });
+
+ try {
+ const response = await fetchNvdJson(context, url, headers, parsedParams.timeoutMs);
+ const statusText = response.statusText || `HTTP ${response.status}`;
+ if (!response.ok) {
+ const text = await response.text();
+ if (parsedParams.failOnUnavailable) throw fromHttpResponse(response, text);
+ return fallbackResult({
+ url,
+ query,
+ status: response.status,
+ statusText,
+ error: text || statusText,
+ });
+ }
+
+ let data: unknown;
+ try {
+ data = await response.json();
+ } catch (error) {
+ if (parsedParams.failOnUnavailable) throw error;
+ return fallbackResult({
+ url,
+ query,
+ status: response.status,
+ statusText: 'Invalid JSON',
+ error: error instanceof Error ? error.message : 'Invalid JSON',
+ });
+ }
+
+ const record = data && typeof data === 'object' ? (data as Record) : {};
+ const vulnerabilities = Array.isArray(record.vulnerabilities) ? record.vulnerabilities : [];
+ const totalResults =
+ typeof record.totalResults === 'number' ? record.totalResults : vulnerabilities.length;
+ const warnings: string[] = [];
+ const dataSource = {
+ name: 'nvd' as const,
+ ok: true,
+ status: response.status,
+ statusText,
+ url,
+ docsUrl: NVD_DOCS_URL,
+ };
+
+ context.logger.info(`[NVD] Returned ${vulnerabilities.length} CVE record(s)`);
+
+ return {
+ ok: true,
+ status: response.status,
+ statusText,
+ url,
+ dataSource,
+ data,
+ vulnerabilities,
+ totalResults,
+ returnedResults: vulnerabilities.length,
+ warnings,
+ summary: {
+ query,
+ ok: true,
+ status: response.status,
+ statusText,
+ totalResults,
+ returnedResults: vulnerabilities.length,
+ warnings,
+ },
+ };
+ } catch (error) {
+ if (parsedParams.failOnUnavailable) throw error;
+ const statusText = classifyFetchError(error);
+ context.logger.warn(
+ `[NVD] CVE query failed: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ return fallbackResult({
+ url,
+ query,
+ status: 0,
+ statusText,
+ });
+ }
+ },
+});
+
+componentRegistry.register(definition);
+
+type NvdCveInput = typeof inputSchema;
+type NvdCveOutput = typeof outputSchema;
+
+export type { NvdCveInput, NvdCveOutput };
+export { definition };
diff --git a/worker/src/components/security/osv.ts b/worker/src/components/security/osv.ts
new file mode 100644
index 00000000..62151028
--- /dev/null
+++ b/worker/src/components/security/osv.ts
@@ -0,0 +1,538 @@
+import { z } from 'zod';
+import {
+ ComponentRetryPolicy,
+ componentRegistry,
+ defineComponent,
+ fromHttpResponse,
+ generateFindingHash,
+ inputs,
+ outputs,
+ parameters,
+ param,
+ port,
+ ValidationError,
+ analyticsResultSchema,
+ type AnalyticsResult,
+ type ExecutionContext,
+} from '@sentris/component-sdk';
+
+const OSV_API_BASE = 'https://api.osv.dev/v1';
+
+const severityRank = {
+ unknown: 0,
+ low: 1,
+ medium: 2,
+ high: 3,
+ critical: 4,
+} as const;
+
+type OsvSeverity = keyof typeof severityRank;
+type AnalyticsSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';
+
+const normalizedPackageSchema = z.object({
+ spec: z.string(),
+ name: z.string(),
+ version: z.string().nullable(),
+ ecosystem: z.string(),
+});
+
+const osvReferenceSchema = z.object({
+ type: z.string().optional(),
+ url: z.string().optional(),
+});
+
+const osvFindingSchema = z.object({
+ packageSpec: z.string(),
+ packageName: z.string().nullable(),
+ version: z.string().nullable(),
+ id: z.string().nullable(),
+ aliases: z.array(z.string()),
+ cves: z.array(z.string()),
+ isMaliciousPackageRecord: z.boolean(),
+ severity: z.enum(['critical', 'high', 'medium', 'low', 'unknown']),
+ summary: z.string().nullable(),
+ published: z.string().nullable(),
+ modified: z.string().nullable(),
+ fixedVersions: z.array(z.string()),
+ references: z.array(osvReferenceSchema),
+});
+
+const summarySchema = z.object({
+ packagesChecked: z.number(),
+ vulnerablePackages: z.number(),
+ findings: z.number(),
+ maliciousPackageRecords: z.number(),
+ countsBySeverity: z.record(z.string(), z.number()),
+});
+
+const inputSchema = inputs({
+ packageSpecs: port(
+ z
+ .array(z.string().min(1))
+ .min(1, 'At least one package spec is required')
+ .describe('Package names with optional versions, for example lodash@4.17.20.'),
+ {
+ label: 'Package Specs',
+ description:
+ 'Package names with optional versions. Scoped npm packages are supported, for example @scope/pkg@1.2.3.',
+ connectionType: { kind: 'list', element: { kind: 'primitive', name: 'text' } },
+ },
+ ),
+});
+
+const parameterSchema = parameters({
+ ecosystem: param(z.string().default('npm').describe('OSV package ecosystem to query.'), {
+ label: 'Ecosystem',
+ editor: 'text',
+ description: 'OSV ecosystem name, for example npm, PyPI, Go, Maven, or crates.io.',
+ }),
+ severityFloor: param(z.enum(['critical', 'high', 'medium', 'low', 'unknown']).default('medium'), {
+ label: 'Severity Floor',
+ editor: 'select',
+ options: [
+ { label: 'Critical', value: 'critical' },
+ { label: 'High', value: 'high' },
+ { label: 'Medium', value: 'medium' },
+ { label: 'Low', value: 'low' },
+ { label: 'Unknown', value: 'unknown' },
+ ],
+ description:
+ 'Known severities below this level are filtered out. Unknown severities are controlled separately.',
+ }),
+ hydrateAdvisories: param(
+ z.boolean().default(true).describe('Fetch full OSV advisory records for returned IDs.'),
+ {
+ label: 'Hydrate Advisories',
+ editor: 'boolean',
+ description:
+ 'OSV querybatch returns advisory IDs only. Hydration fetches summaries, aliases, references, severities, and fixed versions.',
+ },
+ ),
+ maxAdvisoriesPerPackage: param(
+ z
+ .number()
+ .int()
+ .min(1)
+ .max(100)
+ .default(50)
+ .describe('Maximum advisories to process per package.'),
+ {
+ label: 'Max Advisories Per Package',
+ editor: 'number',
+ min: 1,
+ max: 100,
+ },
+ ),
+ includeUnknownSeverity: param(
+ z.boolean().default(true).describe('Keep advisories where OSV does not expose severity.'),
+ {
+ label: 'Include Unknown Severity',
+ editor: 'boolean',
+ description:
+ 'Useful for malicious-package records and ecosystem advisories that do not include CVSS metadata.',
+ },
+ ),
+});
+
+const outputSchema = outputs({
+ findings: port(z.array(osvFindingSchema), {
+ label: 'Findings',
+ description: 'Prioritized OSV advisories for the queried package specs.',
+ connectionType: { kind: 'list', element: { kind: 'primitive', name: 'json' } },
+ }),
+ summary: port(summarySchema, {
+ label: 'Summary',
+ description: 'Counts by package, severity, and malicious-package record status.',
+ connectionType: { kind: 'primitive', name: 'json' },
+ }),
+ packages: port(z.array(normalizedPackageSchema), {
+ label: 'Normalized Packages',
+ description: 'Parsed package specs sent to OSV.',
+ connectionType: { kind: 'list', element: { kind: 'primitive', name: 'json' } },
+ }),
+ rawResults: port(z.unknown(), {
+ label: 'Raw OSV Results',
+ description: 'Raw OSV querybatch response for troubleshooting.',
+ allowAny: true,
+ reason: 'OSV response shape may evolve and can include pagination tokens.',
+ connectionType: { kind: 'primitive', name: 'json' },
+ }),
+ results: port(z.array(analyticsResultSchema()), {
+ label: 'Results',
+ description:
+ 'Analytics-ready findings with scanner, finding_hash, severity, package, and advisory details.',
+ }),
+});
+
+type NormalizedPackage = z.infer;
+type OsvFinding = z.infer;
+
+interface OsvListedVulnerability {
+ id?: string;
+ modified?: string;
+ [key: string]: unknown;
+}
+
+interface OsvQueryBatchResult {
+ results?: {
+ vulns?: OsvListedVulnerability[];
+ next_page_token?: string;
+ }[];
+}
+
+const osvRetryPolicy: ComponentRetryPolicy = {
+ maxAttempts: 3,
+ initialIntervalSeconds: 2,
+ maximumIntervalSeconds: 30,
+ backoffCoefficient: 2.0,
+ nonRetryableErrorTypes: ['ValidationError', 'AuthenticationError', 'ConfigurationError'],
+};
+
+function asRecord(value: unknown): Record {
+ return value && typeof value === 'object' ? (value as Record) : {};
+}
+
+function normalizeSeverity(value: unknown): OsvSeverity {
+ const text = String(value ?? '').toLowerCase();
+ if (text.includes('critical')) return 'critical';
+ if (text.includes('high')) return 'high';
+ if (text.includes('moderate') || text.includes('medium')) return 'medium';
+ if (text.includes('low')) return 'low';
+ return 'unknown';
+}
+
+function severityFromCvssVector(value: unknown): OsvSeverity {
+ const vector = String(value ?? '').toUpperCase();
+ if (!vector.startsWith('CVSS:')) return 'unknown';
+ if (
+ vector.includes('/AV:N') &&
+ vector.includes('/AC:L') &&
+ (vector.includes('/C:H') || vector.includes('/I:H') || vector.includes('/A:H'))
+ ) {
+ return 'high';
+ }
+ if (vector.includes('/C:H') || vector.includes('/I:H') || vector.includes('/A:H')) {
+ return 'medium';
+ }
+ if (vector.includes('/C:L') || vector.includes('/I:L') || vector.includes('/A:L')) {
+ return 'low';
+ }
+ return 'unknown';
+}
+
+function toAnalyticsSeverity(severity: OsvSeverity): AnalyticsSeverity {
+ return severity === 'unknown' ? 'info' : severity;
+}
+
+export function parsePackageSpec(spec: string, defaultEcosystem: string): NormalizedPackage | null {
+ const trimmed = spec.trim();
+ if (!trimmed) return null;
+
+ const versionAt = trimmed.lastIndexOf('@');
+ const hasVersion = versionAt > 0;
+ const name = hasVersion ? trimmed.slice(0, versionAt) : trimmed;
+ const version = hasVersion ? trimmed.slice(versionAt + 1) : null;
+
+ if (!name.trim()) return null;
+
+ return {
+ spec: trimmed,
+ name: name.trim(),
+ version: version?.trim() || null,
+ ecosystem: defaultEcosystem.trim() || 'npm',
+ };
+}
+
+export function inferOsvSeverity(vuln: unknown): OsvSeverity {
+ const record = asRecord(vuln);
+ const candidates: OsvSeverity[] = [];
+ const databaseSpecific = asRecord(record.database_specific ?? record.databaseSpecific);
+ const databaseSeverity = databaseSpecific.severity;
+ if (databaseSeverity) candidates.push(normalizeSeverity(databaseSeverity));
+
+ if (Array.isArray(record.severity)) {
+ for (const item of record.severity) {
+ const severityRecord = asRecord(item);
+ candidates.push(normalizeSeverity(severityRecord.score));
+ candidates.push(severityFromCvssVector(severityRecord.score));
+ }
+ }
+
+ return candidates.sort((a, b) => severityRank[b] - severityRank[a])[0] ?? 'unknown';
+}
+
+export function extractFixedVersions(vuln: unknown): string[] {
+ const fixedVersions = new Set();
+ const record = asRecord(vuln);
+ const affected = Array.isArray(record.affected) ? record.affected : [];
+
+ for (const affectedItem of affected) {
+ const affectedRecord = asRecord(affectedItem);
+ const ranges = Array.isArray(affectedRecord.ranges) ? affectedRecord.ranges : [];
+ for (const range of ranges) {
+ const rangeRecord = asRecord(range);
+ const events = Array.isArray(rangeRecord.events) ? rangeRecord.events : [];
+ for (const event of events) {
+ const fixed = asRecord(event).fixed;
+ if (typeof fixed === 'string' && fixed.trim().length > 0) {
+ fixedVersions.add(fixed.trim());
+ }
+ }
+ }
+ }
+
+ return Array.from(fixedVersions);
+}
+
+function getStringArray(value: unknown): string[] {
+ return Array.isArray(value)
+ ? value.map((item) => String(item)).filter((item) => item.length > 0)
+ : [];
+}
+
+function getReferences(value: unknown): { type?: string; url?: string }[] {
+ return Array.isArray(value)
+ ? value.slice(0, 8).map((item) => {
+ const reference = asRecord(item);
+ return {
+ type: typeof reference.type === 'string' ? reference.type : undefined,
+ url: typeof reference.url === 'string' ? reference.url : undefined,
+ };
+ })
+ : [];
+}
+
+function buildFinding(
+ listedVuln: OsvListedVulnerability,
+ hydratedVuln: unknown,
+ pkg: NormalizedPackage,
+): OsvFinding {
+ const vuln = asRecord(hydratedVuln);
+ const aliases = getStringArray(vuln.aliases);
+ const id =
+ typeof vuln.id === 'string'
+ ? vuln.id
+ : typeof listedVuln.id === 'string'
+ ? listedVuln.id
+ : null;
+
+ return {
+ packageSpec: pkg.spec,
+ packageName: pkg.name,
+ version: pkg.version,
+ id,
+ aliases,
+ cves: aliases.filter((alias) => alias.startsWith('CVE-')),
+ isMaliciousPackageRecord:
+ String(id ?? '').startsWith('MAL-') || aliases.some((alias) => alias.startsWith('MAL-')),
+ severity: inferOsvSeverity(vuln),
+ summary: typeof vuln.summary === 'string' ? vuln.summary : null,
+ published: typeof vuln.published === 'string' ? vuln.published : null,
+ modified:
+ typeof vuln.modified === 'string'
+ ? vuln.modified
+ : typeof listedVuln.modified === 'string'
+ ? listedVuln.modified
+ : null,
+ fixedVersions: extractFixedVersions(vuln),
+ references: getReferences(vuln.references),
+ };
+}
+
+type HttpFetchContext = Pick;
+
+async function fetchJson(
+ context: HttpFetchContext,
+ url: string,
+ init?: RequestInit,
+): Promise {
+ const response = await context.http.fetch(url, init);
+ if (!response.ok) {
+ const text = await response.text();
+ throw fromHttpResponse(response, text);
+ }
+ return response.json();
+}
+
+const definition = defineComponent({
+ id: 'sentris.osv.query',
+ label: 'OSV Dependency Advisory Query',
+ category: 'security',
+ runner: { kind: 'inline' },
+ retryPolicy: osvRetryPolicy,
+ inputs: inputSchema,
+ outputs: outputSchema,
+ parameters: parameterSchema,
+ docs: 'Query OSV.dev for known package vulnerabilities and malicious-package advisories. Supports package/version specs, advisory hydration, severity filtering, and analytics-ready output.',
+ toolProvider: {
+ kind: 'component',
+ name: 'osv_dependency_query',
+ description: 'Package vulnerability and malicious advisory lookup using OSV.dev.',
+ },
+ ui: {
+ slug: 'osv-query',
+ version: '1.0.0',
+ type: 'scan',
+ category: 'security',
+ description:
+ 'Check package/version specs against OSV.dev and return CVEs, fixed versions, references, and analytics-ready findings.',
+ documentationUrl: 'https://google.github.io/osv.dev/api/',
+ icon: 'Shield',
+ author: {
+ name: 'SentrisAI',
+ type: 'sentris',
+ },
+ isLatest: true,
+ deprecated: false,
+ examples: [
+ 'Check npm package versions such as lodash@4.17.20 and minimist@0.0.8.',
+ 'Look up malicious-package advisories for dependency triage.',
+ ],
+ },
+ async execute({ inputs, params }, context) {
+ const parsedParams = parameterSchema.parse(params);
+ const packages = inputs.packageSpecs
+ .map((spec) => parsePackageSpec(spec, parsedParams.ecosystem))
+ .filter((pkg): pkg is NormalizedPackage => Boolean(pkg));
+
+ if (packages.length === 0) {
+ throw new ValidationError('At least one valid package spec is required', {
+ fieldErrors: { packageSpecs: ['At least one valid package spec is required'] },
+ });
+ }
+
+ context.logger.info(`[OSV] Querying ${packages.length} package(s)`);
+ context.emitProgress({
+ message: `Querying OSV for ${packages.length} package(s)`,
+ level: 'info',
+ });
+
+ const rawResults = (await fetchJson(context, `${OSV_API_BASE}/querybatch`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ body: JSON.stringify({
+ queries: packages.map((pkg) => ({
+ package: {
+ name: pkg.name,
+ ecosystem: pkg.ecosystem,
+ },
+ ...(pkg.version ? { version: pkg.version } : {}),
+ })),
+ }),
+ })) as OsvQueryBatchResult;
+
+ const results = Array.isArray(rawResults.results) ? rawResults.results : [];
+ const hydratedCache = new Map();
+ const findings: OsvFinding[] = [];
+
+ for (let index = 0; index < results.length; index++) {
+ const result = results[index];
+ const pkg = packages[index];
+ if (!pkg) continue;
+
+ const listedVulns = Array.isArray(result.vulns)
+ ? result.vulns.slice(0, parsedParams.maxAdvisoriesPerPackage)
+ : [];
+
+ for (const listedVuln of listedVulns) {
+ let advisory: unknown = listedVuln;
+ const advisoryId = typeof listedVuln.id === 'string' ? listedVuln.id : '';
+
+ if (parsedParams.hydrateAdvisories && advisoryId.length > 0) {
+ if (hydratedCache.has(advisoryId)) {
+ advisory = hydratedCache.get(advisoryId);
+ } else {
+ try {
+ advisory = await fetchJson(
+ context,
+ `${OSV_API_BASE}/vulns/${encodeURIComponent(advisoryId)}`,
+ {
+ method: 'GET',
+ headers: { Accept: 'application/json' },
+ },
+ );
+ hydratedCache.set(advisoryId, advisory);
+ } catch (error) {
+ context.logger.warn(
+ `[OSV] Failed to hydrate ${advisoryId}: ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ );
+ }
+ }
+ }
+
+ const finding = buildFinding(listedVuln, advisory, pkg);
+ if (finding.severity === 'unknown' && !parsedParams.includeUnknownSeverity) continue;
+ if (
+ finding.severity !== 'unknown' &&
+ severityRank[finding.severity] < severityRank[parsedParams.severityFloor]
+ ) {
+ continue;
+ }
+ findings.push(finding);
+ }
+ }
+
+ findings.sort(
+ (a, b) =>
+ severityRank[b.severity] - severityRank[a.severity] ||
+ String(b.modified ?? '').localeCompare(String(a.modified ?? '')),
+ );
+
+ const countsBySeverity = findings.reduce>((acc, finding) => {
+ acc[finding.severity] = (acc[finding.severity] ?? 0) + 1;
+ return acc;
+ }, {});
+
+ const vulnerablePackages = new Set(findings.map((finding) => finding.packageSpec));
+ const summary = {
+ packagesChecked: packages.length,
+ vulnerablePackages: vulnerablePackages.size,
+ findings: findings.length,
+ maliciousPackageRecords: findings.filter((finding) => finding.isMaliciousPackageRecord)
+ .length,
+ countsBySeverity,
+ };
+
+ const analyticsResults: AnalyticsResult[] = findings.map((finding) => ({
+ scanner: 'osv',
+ finding_hash: generateFindingHash(
+ finding.id ?? 'unknown-osv-advisory',
+ finding.packageSpec,
+ finding.version ?? '',
+ ),
+ severity: toAnalyticsSeverity(finding.severity),
+ asset_key: finding.packageSpec,
+ vulnerability_id: finding.id ?? undefined,
+ package_name: finding.packageName ?? undefined,
+ installed_version: finding.version ?? undefined,
+ fixed_versions: finding.fixedVersions,
+ aliases: finding.aliases,
+ cves: finding.cves,
+ title: finding.summary ?? undefined,
+ malicious_package_record: finding.isMaliciousPackageRecord,
+ }));
+
+ context.logger.info(`[OSV] Found ${findings.length} advisory finding(s)`);
+
+ return {
+ findings,
+ summary,
+ packages,
+ rawResults,
+ results: analyticsResults,
+ };
+ },
+});
+
+componentRegistry.register(definition);
+
+type OsvInput = typeof inputSchema;
+type OsvOutput = typeof outputSchema;
+
+export type { OsvInput, OsvOutput };
+export { definition };
diff --git a/worker/src/temporal/__tests__/workflow-diagnostics.test.ts b/worker/src/temporal/__tests__/workflow-diagnostics.test.ts
new file mode 100644
index 00000000..b77fd328
--- /dev/null
+++ b/worker/src/temporal/__tests__/workflow-diagnostics.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, test } from 'bun:test';
+
+import { shouldLogWorkflowDiagnostics } from '../workflow-diagnostics';
+
+describe('workflow diagnostics', () => {
+ test('does not throw when process is unavailable in the workflow sandbox', () => {
+ const originalProcess = (globalThis as { process?: unknown }).process;
+
+ try {
+ Reflect.deleteProperty(globalThis, 'process');
+
+ expect(() => shouldLogWorkflowDiagnostics()).not.toThrow();
+ expect(shouldLogWorkflowDiagnostics()).toBe(false);
+ } finally {
+ (globalThis as { process?: unknown }).process = originalProcess;
+ }
+ });
+});
diff --git a/worker/src/temporal/activities/__tests__/mcp.activity.test.ts b/worker/src/temporal/activities/__tests__/mcp.activity.test.ts
index a8aa9cb6..79dc5eb8 100644
--- a/worker/src/temporal/activities/__tests__/mcp.activity.test.ts
+++ b/worker/src/temporal/activities/__tests__/mcp.activity.test.ts
@@ -14,6 +14,7 @@ mock.module('node:util', () => ({
// Import AFTER mocks
import {
+ buildInternalMcpUrl,
registerComponentToolActivity,
registerRemoteMcpActivity,
registerLocalMcpActivity,
@@ -65,6 +66,15 @@ describe('MCP Activities', () => {
// ── callInternalApi (tested indirectly) ──────────────────────────────────
describe('callInternalApi error handling', () => {
+ it('does not double-prefix /api/v1 when SENTRIS_API_BASE_URL is already versioned', () => {
+ expect(buildInternalMcpUrl('http://localhost:3211/api/v1', 'cleanup')).toBe(
+ 'http://localhost:3211/api/v1/internal/mcp/cleanup',
+ );
+ expect(buildInternalMcpUrl('http://localhost:3211', 'cleanup')).toBe(
+ 'http://localhost:3211/api/v1/internal/mcp/cleanup',
+ );
+ });
+
it('throws non-retryable ApplicationFailure when INTERNAL_SERVICE_TOKEN is missing', async () => {
delete process.env.INTERNAL_SERVICE_TOKEN;
diff --git a/worker/src/temporal/activities/mcp.activity.ts b/worker/src/temporal/activities/mcp.activity.ts
index 4c8d481e..42b70be2 100644
--- a/worker/src/temporal/activities/mcp.activity.ts
+++ b/worker/src/temporal/activities/mcp.activity.ts
@@ -23,6 +23,16 @@ function normalizeBaseUrl(url: string): string {
return url.endsWith('/') ? url.slice(0, -1) : url;
}
+function normalizeVersionedApiBaseUrl(url: string): string {
+ const baseUrl = normalizeBaseUrl(url);
+ return baseUrl.endsWith('/api/v1') ? baseUrl : `${baseUrl}/api/v1`;
+}
+
+export function buildInternalMcpUrl(baseUrl: string, path: string): string {
+ const normalizedPath = path.replace(/^\/+/, '');
+ return `${normalizeVersionedApiBaseUrl(baseUrl)}/internal/mcp/${normalizedPath}`;
+}
+
async function callInternalApi(path: string, body: any) {
const internalToken = process.env.INTERNAL_SERVICE_TOKEN;
if (!internalToken) {
@@ -32,8 +42,7 @@ async function callInternalApi(path: string, body: any) {
);
}
- const baseUrl = normalizeBaseUrl(DEFAULT_API_BASE_URL);
- const url = `${baseUrl}/api/v1/internal/mcp/${path}`;
+ const url = buildInternalMcpUrl(DEFAULT_API_BASE_URL, path);
const response = await fetch(url, {
method: 'POST',
headers: {
diff --git a/worker/src/temporal/workflow-diagnostics.ts b/worker/src/temporal/workflow-diagnostics.ts
index 28f5f522..cac7d48e 100644
--- a/worker/src/temporal/workflow-diagnostics.ts
+++ b/worker/src/temporal/workflow-diagnostics.ts
@@ -1,5 +1,5 @@
export function shouldLogWorkflowDiagnostics(): boolean {
- return process.env.SENTRIS_DEBUG_WORKFLOW === '1';
+ return typeof process !== 'undefined' && process.env.SENTRIS_DEBUG_WORKFLOW === '1';
}
export function workflowDiagnosticLog(...args: Parameters): void {