#!/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 === 'plinyos.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 === 'plinyos.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 === 'plinyos.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 === 'plinyos.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 === 'plinyos.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); });