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>
60 lines
3.1 KiB
TypeScript
60 lines
3.1 KiB
TypeScript
/**
|
|
* Secret / credential redaction (server response + audit + SSE boundary). Pins the load-bearing scrub,
|
|
* including the 7th-pass fix: a `scheme://user:pass@host` login URL (the shape approval audit target/
|
|
* action strings take for a gated credential tool) must not leak its password over the REST endpoint or
|
|
* the SSE feed.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { redactString, redactSecrets } from '../redact.js';
|
|
|
|
describe('redactString — URL userinfo (basic-auth) scrub', () => {
|
|
it('strips the password from a scheme://user:pass@host login URL (all schemes)', () => {
|
|
expect(redactString('https://admin:hunter2@vpn.corp/login')).toBe('https://[redacted]@vpn.corp/login');
|
|
expect(redactString('ssh://root:toor@10.0.0.5')).toBe('ssh://[redacted]@10.0.0.5');
|
|
expect(redactString('ftp://svc:S3cr3t@files.internal/x')).toBe('ftp://[redacted]@files.internal/x');
|
|
});
|
|
|
|
it('covers a password that itself contains @ (redacts up to the LAST @ before the host)', () => {
|
|
expect(redactString('https://svcacct:S3cr3t-P@ss!23@intranet.corp/login'))
|
|
.toBe('https://[redacted]@intranet.corp/login');
|
|
});
|
|
|
|
it('leaves credential-free URLs and non-authority URIs untouched', () => {
|
|
expect(redactString('https://example.com/path?x=1')).toBe('https://example.com/path?x=1');
|
|
expect(redactString('visit https://host/a@b/c for docs')).toBe('visit https://host/a@b/c for docs'); // @ after a path segment, not userinfo
|
|
expect(redactString('mailto:alice@example.com')).toBe('mailto:alice@example.com'); // no ://, not userinfo
|
|
});
|
|
|
|
it('still redacts the pre-existing secret shapes (regression)', () => {
|
|
expect(redactString('key AKIAIOSFODNN7EXAMPLE here')).toContain('[redacted]');
|
|
expect(redactString('token=abc123def456ghi')).toBe('token=[redacted]');
|
|
expect(redactString('Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')).toContain('Bearer [redacted]');
|
|
});
|
|
});
|
|
|
|
describe('redactSecrets — approval-audit record shape', () => {
|
|
it('scrubs a userinfo password in the audit target/action a gated credential tool would record', () => {
|
|
// mirrors what Arsenal.execute() records for password_spray({ url: 'https://user:pass@host/login' })
|
|
const record = {
|
|
tool: 'password_spray',
|
|
risk: 'credential',
|
|
target: 'https://admin:hunter2@vpn.corp/login',
|
|
action: 'password_spray (url=https://admin:hunter2@vpn.corp/login username=admin) → https://admin:hunter2@vpn.corp/login',
|
|
outcome: 'allowed-preapproved',
|
|
};
|
|
const out = redactSecrets(record) as typeof record;
|
|
expect(out.target).toBe('https://[redacted]@vpn.corp/login');
|
|
expect(out.action).not.toContain('hunter2');
|
|
expect(out.action).toContain('[redacted]');
|
|
});
|
|
|
|
it('redacts a key named like a secret and recurses through arrays/objects', () => {
|
|
const out = redactSecrets({ password: 'p', nested: [{ token: 't', url: 'https://u:pw@h/x' }] }) as {
|
|
password: string; nested: Array<{ token: string; url: string }>;
|
|
};
|
|
expect(out.password).toBe('[redacted]');
|
|
expect(out.nested[0].token).toBe('[redacted]');
|
|
expect(out.nested[0].url).toBe('https://[redacted]@h/x');
|
|
});
|
|
});
|