Adds three coordinated capabilities and tightens two defaults on purpose — this is safety hardening, NOT a backward-compatible drop-in (see "Tightened defaults"). Fifteen independent adversarial review passes; findings fixed and test-pinned. - Pack-hunt safety: a strict-majority adjudicator with a comment/string/regex/ raw-string/heredoc/POD-aware cite-check (language-gated across C/C++/Py/Rust/Go/ Java/JS/TS/shell/SQL/PHP/Ruby/Perl) so a hallucinated killing-guard can't refute a real finding — the cited comparison's operator must match source, and a string/regex/heredoc/generic-arglist/template-interpolation is blanked before matching. Plus a rate-limit/degraded-health tracker, a global LLM semaphore, and a PackBoard with an atomic claim CAS + snapshot-on-emit/clone-on-ingest. - Opt-in Kali+ arsenal (T3MP3ST_FULL_ARSENAL, OFF by default): 35 default tools (31 built-in + 4 external-CLI wrappers) -> 83 with the catalog armed (verify-claims CLAIM 4). Most adapters need the underlying binary installed. metasploit / hydra / bloodhound are never minted through the generic factory. - Capability-approval + spicy-action gate for the NEW dangerous arsenal only (pre-existing built-ins keep running freely): approve-once, fail-safe deny when unattended, redacted audit trail over SSE. T3MP3ST_GATE_BUILTINS=1 extends it to the spicy built-in probes. Audit redaction is a pure, unit-tested module. Tightened defaults (intentional, ON by default — NOT backward-compatible): - Egress containment: once a mission target is set, built-in networked tools refuse off-scope public hosts (not the target/subdomains, not loopback/private) with SCOPE DENIED — keeps the agent on authorized scope. - Role-specialized operator toolkits: each archetype gets a curated default tool menu (scanner -> scan tools, exploiter -> sqli/xss/etc). Tools stay registered team-wide; the per-role menu is the operator's working set. UX (from live-tester feedback): a one-click "Approve & run" modal when a standard Hunt blocks on an external-target receipt (was a dead-end); bounded-parallel benchmark file ingestion; the deterministic mission-gate chip co-located with the subjective SITREP pace narration (no more "behind schedule" next to a clean gate); and click-through finding detail + evidence in the UI (was export-only). Docs: README + INTEGRITY_LEDGER made re-derivable — honest 35/83 tool count, the egress-containment default documented, and the Cybench number reconciled to the single verify-claims-enforced 23/40 = 58% (dropped the stale/uncited 21 and 26). README voice pass: killed the underselling `Successful Exploits: 0` disqualifier (read as "never popped anything" — false, given CVE-Zero + the live disclosure pipeline); swagger now rests on receipts, not vibes. Also corrected the operator status — the downstream operators run the SAME real tool-backed ReAct loop as recon (not "orchestration stubs"); the honest limit is that the coordinated swarm is unproven (single-agent is the benchmarked path), not that they're scaffolding. Gates: 292 tests, tsc + lint clean, verify-claims 20/20, arsenal:smoke 125/0, disclosure-CLI self-test 11/11. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
223 lines
12 KiB
JavaScript
223 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
import process from 'node:process';
|
|
|
|
const baseUrl = (process.env.T3MP3ST_API_URL || 'http://127.0.0.1:3333').replace(/\/$/, '');
|
|
const checks = [];
|
|
const warnings = [];
|
|
|
|
const localTarget = 'http://127.0.0.1:3333/__synthetic-owned-local-range';
|
|
const localScope = ['127.0.0.1', 'local-lab', localTarget];
|
|
|
|
function record(name, passed, detail = '', evidence = undefined) {
|
|
checks.push({ name, passed: Boolean(passed), detail, evidence });
|
|
}
|
|
|
|
function warn(message) {
|
|
warnings.push(message);
|
|
}
|
|
|
|
async function request(method, path, body) {
|
|
const response = await fetch(`${baseUrl}${path}`, {
|
|
method,
|
|
headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
const text = await response.text();
|
|
let data;
|
|
try {
|
|
data = text ? JSON.parse(text) : {};
|
|
} catch {
|
|
data = { raw: text };
|
|
}
|
|
return { status: response.status, ok: response.ok, data };
|
|
}
|
|
|
|
const get = path => request('GET', path);
|
|
const post = (path, body) => request('POST', path, body);
|
|
const patch = (path, body) => request('PATCH', path, body);
|
|
|
|
function countItems(value) {
|
|
return Array.isArray(value) ? value.length : 0;
|
|
}
|
|
|
|
async function main() {
|
|
const startedAt = new Date().toISOString();
|
|
|
|
const health = await get('/health');
|
|
record('API health is operational', health.ok && health.data.status === 'operational', `${health.status} ${health.data.status || 'unknown'}`);
|
|
|
|
const preflight = await get('/api/preflight');
|
|
record('Preflight supports local exploit-chain smoke', preflight.ok && preflight.data.ok === true, `${preflight.data.score ?? 'n/a'}/100`);
|
|
if (preflight.ok) {
|
|
const missing = (preflight.data.tools || [])
|
|
.filter(tool => !tool.available && ['nmap', 'dig'].includes(tool.name))
|
|
.map(tool => tool.name);
|
|
if (missing.length) warn(`Optional recon tools missing: ${missing.join(', ')}`);
|
|
}
|
|
|
|
const draft = await post('/api/mission-drafts', {
|
|
title: 'Exploit Chain Smoke: Owned Local Synthetic Range',
|
|
objective: 'Authorized local web/API exploit-chain smoke test. Exercise the mission approval, ledger, and gate loop against a synthetic local HTTP endpoint, preserve evidence, and do not touch external targets.',
|
|
scope: localScope,
|
|
constraints: 'Local loopback and synthetic fixtures only. No production writes. No third-party targets. No real credential use.',
|
|
source: 'human',
|
|
urgency: 'normal',
|
|
opsecPreference: 'normal',
|
|
});
|
|
record('Mission draft created', draft.status === 201 && draft.data.id, draft.data.id || draft.data.error);
|
|
|
|
const route = draft.data.id
|
|
? await post('/api/route-preview', { draftId: draft.data.id })
|
|
: { ok: false, data: {} };
|
|
const operationDraft = route.data.operationDraft || {};
|
|
record('Route preview emits operation contract', route.ok && operationDraft.schema_version === 't3mp3st.operation/v1', operationDraft.schema_version || route.data.error);
|
|
record('Route family is web/API for synthetic local range', route.ok && route.data.route?.family === 'web_api', route.data.route?.family || route.data.error);
|
|
|
|
const approvalRequest = await post('/api/approvals/request', {
|
|
action: 'mission_execution',
|
|
target: operationDraft.target || localTarget,
|
|
reason: 'Exploit-chain smoke receipt for synthetic local loopback range.',
|
|
operationDraft,
|
|
source: 'human',
|
|
});
|
|
record('Mission execution receipt requested', approvalRequest.status === 201 && approvalRequest.data.status === 'pending', approvalRequest.data.id || approvalRequest.data.error);
|
|
|
|
const approval = approvalRequest.data.id
|
|
? await post(`/api/approvals/${approvalRequest.data.id}/approve`, { approvedBy: 'exploit-chain-smoke', ttlMinutes: 5 })
|
|
: { ok: false, data: {} };
|
|
record('Mission execution receipt approved', approval.ok && approval.data.status === 'approved', approval.data.id || approval.data.error);
|
|
|
|
// NOTE: The nine "Pliny Specials" backend routes this drill used to walk
|
|
// (/api/pliny/leviathan/engage, sphinx/validate, gorgon/strike,
|
|
// typhon/inject, cerberus/escalate, griffin/harvest, simurgh/hunt,
|
|
// hydra/orchestrate, arachne/chain) were removed from src/server.ts on
|
|
// 2026-06-30 as deprecated dead theater, so there is no backend chain left
|
|
// to smoke. This drill now covers what actually ships: the mission
|
|
// contract + ledger + gate loop (draft -> route -> receipt -> evidence ->
|
|
// finding -> hypothesis -> retest -> bundle -> gate -> learning review).
|
|
|
|
const chainEvidence = await post('/api/evidence', {
|
|
missionId: operationDraft.mission_id,
|
|
operationId: operationDraft.operation_id,
|
|
type: 'command',
|
|
title: 'Exploit-chain smoke summary',
|
|
summary: 'Mission contract and ledger loop exercised end-to-end against the synthetic local range: draft -> route -> receipt -> evidence -> finding -> hypothesis -> retest -> bundle -> gate.',
|
|
source: 'tool',
|
|
command: 'npm run exploit:smoke',
|
|
resourceIds: ['owasp-wstg', 'owasp-api-top10-2023', 'cwe-top25-2025'],
|
|
});
|
|
record('Evidence ledger records chain smoke summary', chainEvidence.status === 201 && chainEvidence.data.id, chainEvidence.data.id || chainEvidence.data.error);
|
|
|
|
const finding = await post('/api/findings', {
|
|
missionId: operationDraft.mission_id,
|
|
operationId: operationDraft.operation_id,
|
|
family: operationDraft.family || 'web_api',
|
|
title: 'Exploit-chain smoke path is traceable',
|
|
target: 'local synthetic range',
|
|
claim: 'The exploit-chain smoke produced a traceable mission contract and ledger loop: draft, route, receipt, evidence, finding, hypothesis, and retest.',
|
|
impact: 'Operators can verify the mission/ledger/gate loop without touching external systems.',
|
|
severity: 'high',
|
|
confidence: 0.86,
|
|
evidenceIds: chainEvidence.data.id ? [chainEvidence.data.id] : [],
|
|
resourceIds: ['owasp-wstg', 'owasp-api-top10-2023', 'cwe-top25-2025'],
|
|
recommendedFix: 'Keep the mission-contract smoke in the ship gate so the ledger and gate wiring stays proven before real authorized work.',
|
|
acceptanceCriteria: ['Evidence is linked', 'Retest passes', 'Mission gate reaches ready or explains gaps'],
|
|
});
|
|
record('Finding ledger records chain smoke claim', finding.status === 201 && finding.data.id, finding.data.id || finding.data.error);
|
|
|
|
const hypothesis = await post('/api/hypotheses', {
|
|
missionId: operationDraft.mission_id,
|
|
operationId: operationDraft.operation_id,
|
|
family: operationDraft.family || 'web_api',
|
|
target: 'local synthetic range',
|
|
claim: 'The exploit-chain smoke remains traceable from hypothesis to evidence, finding, and retest.',
|
|
rationale: 'Mission readiness should require the same hypothesis-led proof loop used by real zero-day hunts, even for local synthetic drills.',
|
|
status: finding.data.id ? 'promoted' : 'supported',
|
|
confidence: 0.86,
|
|
evidenceForIds: chainEvidence.data.id ? [chainEvidence.data.id] : [],
|
|
findingIds: finding.data.id ? [finding.data.id] : [],
|
|
nextTests: ['Evidence is linked', 'Retest passes', 'Mission gate reaches ready'],
|
|
});
|
|
record('Hypothesis ledger links chain smoke proof loop', hypothesis.status === 201 && hypothesis.data.id && hypothesis.data.findingIds?.includes(finding.data.id), hypothesis.data.id || hypothesis.data.error);
|
|
|
|
const retest = finding.data.id
|
|
? await post(`/api/findings/${finding.data.id}/retest`, {
|
|
method: 'Re-run npm run exploit:smoke and verify the mission-contract and ledger checks pass with no external target.',
|
|
acceptanceCriteria: ['Evidence is linked', 'Retest passes', 'Mission gate reaches ready'],
|
|
})
|
|
: { status: 0, data: {} };
|
|
record('Retest queued for full-chain smoke', retest.status === 201 && retest.data.id, retest.data.id || retest.data.error);
|
|
|
|
const completedRetest = retest.data.id
|
|
? await patch(`/api/retests/${retest.data.id}`, { status: 'passed', resultSummary: 'Full exploit-chain smoke passed against synthetic local range.' })
|
|
: { ok: false, data: {} };
|
|
record('Retest completed for full-chain smoke', completedRetest.ok && completedRetest.data.status === 'passed', completedRetest.data.status || completedRetest.data.error);
|
|
|
|
const bundle = await post('/api/mission-bundles', { operationDraft });
|
|
record('Mission bundle includes exploit-chain ledgers', bundle.ok && bundle.data.schema_version === 't3mp3st.mission_bundle/v1' && countItems(bundle.data.ledgers?.evidence) > 0 && countItems(bundle.data.ledgers?.findings) > 0, bundle.data.handoff?.humanSummary || bundle.data.error);
|
|
|
|
const gate = await post('/api/mission-gate', { operationDraft });
|
|
record('Mission gate is ready after receipt, evidence, and retest', gate.ok && gate.data.schema_version === 't3mp3st.mission_gate/v1' && gate.data.status === 'ready' && gate.data.score >= 80, `${gate.data.status || 'unknown'} ${gate.data.score ?? 'n/a'}/100`);
|
|
|
|
const learningReview = await post('/api/learning/run-review', {
|
|
missionId: operationDraft.mission_id,
|
|
operationId: operationDraft.operation_id,
|
|
family: operationDraft.family,
|
|
});
|
|
record('Learning review proposes inspectable memory', learningReview.status === 201 && learningReview.data.schema_version === 't3mp3st.learning_review/v1' && countItems(learningReview.data.proposals) > 0, `${countItems(learningReview.data.proposals)} proposal(s)`);
|
|
|
|
const proposalId = learningReview.data.proposals?.[0]?.id;
|
|
const acceptedMemory = proposalId
|
|
? await post(`/api/memory/proposals/${proposalId}/accept`, {})
|
|
: { ok: false, data: {} };
|
|
record('Memory proposal can be explicitly accepted', acceptedMemory.ok && acceptedMemory.data.entry?.id && acceptedMemory.data.proposal?.status === 'accepted', acceptedMemory.data.entry?.id || acceptedMemory.data.error);
|
|
|
|
const capsule = await get('/api/memory/capsule');
|
|
record('Memory capsule exposes accepted entries', capsule.ok && capsule.data.schema_version === 't3mp3st.memory_capsule/v1' && (capsule.data.entries || []).some(entry => entry.id === acceptedMemory.data.entry?.id), `${countItems(capsule.data.entries)} accepted memory entries`);
|
|
|
|
const proposalListBeforeRepeat = await get('/api/memory/proposals');
|
|
const proposalCountBeforeRepeat = countItems(proposalListBeforeRepeat.data.proposals);
|
|
const repeatedLearningReview = await post('/api/learning/run-review', {
|
|
missionId: operationDraft.mission_id,
|
|
operationId: operationDraft.operation_id,
|
|
family: operationDraft.family,
|
|
});
|
|
const proposalListAfterRepeat = await get('/api/memory/proposals');
|
|
const proposalCountAfterRepeat = countItems(proposalListAfterRepeat.data.proposals);
|
|
const reinforced = (repeatedLearningReview.data.proposals || []).some(proposal => (proposal.observationCount || 1) > 1);
|
|
record('Repeated learning review reinforces instead of duplicating', repeatedLearningReview.status === 201 && proposalCountAfterRepeat === proposalCountBeforeRepeat && reinforced, `${proposalCountAfterRepeat} proposals / reinforced=${reinforced}`);
|
|
|
|
const failed = checks.filter(check => !check.passed);
|
|
const report = {
|
|
drill: 't3mp3st-full-exploit-chain-smoke',
|
|
baseUrl,
|
|
target: 'synthetic-local-range',
|
|
startedAt,
|
|
finishedAt: new Date().toISOString(),
|
|
passed: failed.length === 0,
|
|
summary: {
|
|
gateStatus: gate.data.status || 'unknown',
|
|
gateScore: gate.data.score ?? null,
|
|
memoryProposals: countItems(learningReview.data.proposals),
|
|
acceptedMemoryEntry: acceptedMemory.data.entry?.id || null,
|
|
dedupedLearning: proposalCountAfterRepeat === proposalCountBeforeRepeat,
|
|
},
|
|
checks,
|
|
warnings,
|
|
};
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
if (failed.length) process.exit(1);
|
|
}
|
|
|
|
main().catch(error => {
|
|
console.error(JSON.stringify({
|
|
drill: 't3mp3st-full-exploit-chain-smoke',
|
|
baseUrl,
|
|
target: 'synthetic-local-range',
|
|
passed: false,
|
|
fatal: error instanceof Error ? error.message : String(error),
|
|
}, null, 2));
|
|
process.exit(1);
|
|
});
|