@panguard-ai/panguard-guard 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/report-agent.d.ts +23 -2
- package/dist/agent/report-agent.d.ts.map +1 -1
- package/dist/agent/report-agent.js +80 -15
- package/dist/agent/report-agent.js.map +1 -1
- package/dist/agent/respond/action-manifest.d.ts +13 -3
- package/dist/agent/respond/action-manifest.d.ts.map +1 -1
- package/dist/agent/respond/action-manifest.js +40 -13
- package/dist/agent/respond/action-manifest.js.map +1 -1
- package/dist/agent/respond-agent.d.ts +1 -1
- package/dist/agent/respond-agent.d.ts.map +1 -1
- package/dist/agent/respond-agent.js +2 -2
- package/dist/agent/respond-agent.js.map +1 -1
- package/dist/audit/attribution.d.ts +51 -0
- package/dist/audit/attribution.d.ts.map +1 -0
- package/dist/audit/attribution.js +69 -0
- package/dist/audit/attribution.js.map +1 -0
- package/dist/audit/audit-chain.d.ts +110 -0
- package/dist/audit/audit-chain.d.ts.map +1 -0
- package/dist/audit/audit-chain.js +316 -0
- package/dist/audit/audit-chain.js.map +1 -0
- package/dist/audit/audit-key.d.ts +27 -0
- package/dist/audit/audit-key.d.ts.map +1 -0
- package/dist/audit/audit-key.js +132 -0
- package/dist/audit/audit-key.js.map +1 -0
- package/dist/audit/hash-chain.d.ts +96 -0
- package/dist/audit/hash-chain.d.ts.map +1 -0
- package/dist/audit/hash-chain.js +176 -0
- package/dist/audit/hash-chain.js.map +1 -0
- package/dist/audit/index.d.ts +15 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +15 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/cli/index.js +27 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard/index.d.ts +26 -5
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +186 -43
- package/dist/dashboard/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/integrity.d.ts +80 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +208 -0
- package/dist/integrity.js.map +1 -0
- package/dist/rule-loader.d.ts.map +1 -1
- package/dist/rule-loader.js +8 -2
- package/dist/rule-loader.js.map +1 -1
- package/package.json +12 -8
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit HMAC key management.
|
|
3
|
+
*
|
|
4
|
+
* The audit chain is signed with a per-machine HMAC key. We resolve it in two
|
|
5
|
+
* tiers, generate-once + reuse:
|
|
6
|
+
* 1. Keychain-first: the security-hardening credential store (service
|
|
7
|
+
* 'panguard-audit'). On macOS/Linux/Windows this is the OS-protected store
|
|
8
|
+
* where available; otherwise it falls back to an AES-256-GCM encrypted file
|
|
9
|
+
* under ~/.panguard/credentials, keyed by machine entropy.
|
|
10
|
+
* 2. Plain-file fallback: ~/.panguard/audit-key (randomBytes(32), dir 0700,
|
|
11
|
+
* file 0600) for environments where the credential store is unavailable.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the get-or-create discipline of threat-cloud/client-id.ts.
|
|
14
|
+
*
|
|
15
|
+
* @module @panguard-ai/panguard-guard/audit/audit-key
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { randomBytes } from 'node:crypto';
|
|
21
|
+
import { createLogger } from '@panguard-ai/core';
|
|
22
|
+
import { EncryptedFileCredentialStore } from '@panguard-ai/security-hardening';
|
|
23
|
+
const logger = createLogger('panguard-guard:audit-key');
|
|
24
|
+
/** Credential-store service name for the audit key. */
|
|
25
|
+
const AUDIT_SERVICE = 'panguard-audit';
|
|
26
|
+
/** Credential-store account name. */
|
|
27
|
+
const AUDIT_ACCOUNT = 'hmac-key';
|
|
28
|
+
/** Key size in bytes (256-bit HMAC key). */
|
|
29
|
+
const KEY_BYTES = 32;
|
|
30
|
+
/** Directory holding panguard local secrets. */
|
|
31
|
+
const PANGUARD_DIR = join(homedir(), '.panguard');
|
|
32
|
+
/** Credential-store directory used by the keychain-first tier. */
|
|
33
|
+
const CRED_STORE_DIR = join(PANGUARD_DIR, 'credentials');
|
|
34
|
+
/**
|
|
35
|
+
* Plain-file fallback path. Same-user-readable: an attacker running as THIS user
|
|
36
|
+
* can read this key and recompute a consistent chain. This is tamper EVIDENCE,
|
|
37
|
+
* not tamper PREVENTION — remote anchoring (publish chain head to Threat Cloud)
|
|
38
|
+
* is the enterprise upgrade that closes the same-user gap. See SECURITY.md.
|
|
39
|
+
*/
|
|
40
|
+
const FALLBACK_KEY_PATH = join(PANGUARD_DIR, 'audit-key');
|
|
41
|
+
/** Cached key so repeated calls in one process don't re-read disk. */
|
|
42
|
+
let cachedKey = null;
|
|
43
|
+
/**
|
|
44
|
+
* Get (or generate-once and persist) the audit HMAC key.
|
|
45
|
+
*
|
|
46
|
+
* Never throws: if the keychain tier is unavailable it falls through to the
|
|
47
|
+
* file fallback; if even the file cannot be persisted, an in-memory key is
|
|
48
|
+
* returned so audit signing still works for the life of the process.
|
|
49
|
+
*/
|
|
50
|
+
export async function getAuditKey() {
|
|
51
|
+
if (cachedKey)
|
|
52
|
+
return cachedKey;
|
|
53
|
+
// Tier 1: keychain-first via the credential store.
|
|
54
|
+
const fromStore = await tryReadFromStore();
|
|
55
|
+
if (fromStore) {
|
|
56
|
+
cachedKey = fromStore;
|
|
57
|
+
return fromStore;
|
|
58
|
+
}
|
|
59
|
+
// Tier 2: plain-file fallback (0700 dir / 0600 file).
|
|
60
|
+
const fromFile = tryReadFromFile();
|
|
61
|
+
if (fromFile) {
|
|
62
|
+
cachedKey = fromFile;
|
|
63
|
+
return fromFile;
|
|
64
|
+
}
|
|
65
|
+
// Generate once. Try to persist to the store first, then the file.
|
|
66
|
+
const fresh = randomBytes(KEY_BYTES);
|
|
67
|
+
const persistedToStore = await tryWriteToStore(fresh);
|
|
68
|
+
if (!persistedToStore) {
|
|
69
|
+
tryWriteToFile(fresh);
|
|
70
|
+
}
|
|
71
|
+
cachedKey = fresh;
|
|
72
|
+
return fresh;
|
|
73
|
+
}
|
|
74
|
+
/** Read the key from the credential store, or null if absent/unavailable. */
|
|
75
|
+
async function tryReadFromStore() {
|
|
76
|
+
try {
|
|
77
|
+
const store = new EncryptedFileCredentialStore(CRED_STORE_DIR);
|
|
78
|
+
const hex = await store.get(AUDIT_SERVICE, AUDIT_ACCOUNT);
|
|
79
|
+
if (hex && /^[0-9a-f]+$/i.test(hex) && hex.length === KEY_BYTES * 2) {
|
|
80
|
+
return Buffer.from(hex, 'hex');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
logger.warn(`Audit key store read unavailable: ${errMsg(err)}`);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** Persist the key to the credential store. Returns true on success. */
|
|
89
|
+
async function tryWriteToStore(key) {
|
|
90
|
+
try {
|
|
91
|
+
const store = new EncryptedFileCredentialStore(CRED_STORE_DIR);
|
|
92
|
+
await store.set(AUDIT_SERVICE, AUDIT_ACCOUNT, key.toString('hex'));
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger.warn(`Audit key store write unavailable: ${errMsg(err)}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Read the key from the plain-file fallback, or null if absent/invalid. */
|
|
101
|
+
function tryReadFromFile() {
|
|
102
|
+
try {
|
|
103
|
+
if (!existsSync(FALLBACK_KEY_PATH))
|
|
104
|
+
return null;
|
|
105
|
+
const hex = readFileSync(FALLBACK_KEY_PATH, 'utf-8').trim();
|
|
106
|
+
if (hex && /^[0-9a-f]+$/i.test(hex) && hex.length === KEY_BYTES * 2) {
|
|
107
|
+
return Buffer.from(hex, 'hex');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
logger.warn(`Audit key file read failed: ${errMsg(err)}`);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/** Persist the key to the plain-file fallback (0700 dir / 0600 file). */
|
|
116
|
+
function tryWriteToFile(key) {
|
|
117
|
+
try {
|
|
118
|
+
mkdirSync(PANGUARD_DIR, { recursive: true, mode: 0o700 });
|
|
119
|
+
writeFileSync(FALLBACK_KEY_PATH, key.toString('hex'), { mode: 0o600 });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
logger.warn(`Audit key file persist failed (using in-memory key): ${errMsg(err)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function errMsg(err) {
|
|
126
|
+
return err instanceof Error ? err.message : String(err);
|
|
127
|
+
}
|
|
128
|
+
/** Reset the in-process cache. Test-only seam. */
|
|
129
|
+
export function __resetAuditKeyCacheForTests() {
|
|
130
|
+
cachedKey = null;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=audit-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-key.js","sourceRoot":"","sources":["../../src/audit/audit-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,4BAA4B,EAAE,MAAM,iCAAiC,CAAC;AAE/E,MAAM,MAAM,GAAG,YAAY,CAAC,0BAA0B,CAAC,CAAC;AAExD,uDAAuD;AACvD,MAAM,aAAa,GAAG,gBAAgB,CAAC;AACvC,qCAAqC;AACrC,MAAM,aAAa,GAAG,UAAU,CAAC;AACjC,4CAA4C;AAC5C,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB,gDAAgD;AAChD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;AAClD,kEAAkE;AAClE,MAAM,cAAc,GAAG,IAAI,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;AACzD;;;;;GAKG;AACH,MAAM,iBAAiB,GAAG,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;AAE1D,sEAAsE;AACtE,IAAI,SAAS,GAAkB,IAAI,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhC,mDAAmD;IACnD,MAAM,SAAS,GAAG,MAAM,gBAAgB,EAAE,CAAC;IAC3C,IAAI,SAAS,EAAE,CAAC;QACd,SAAS,GAAG,SAAS,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,sDAAsD;IACtD,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC;IACnC,IAAI,QAAQ,EAAE,CAAC;QACb,SAAS,GAAG,QAAQ,CAAC;QACrB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,mEAAmE;IACnE,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACrC,MAAM,gBAAgB,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC;IACtD,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,cAAc,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IACD,SAAS,GAAG,KAAK,CAAC;IAClB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,6EAA6E;AAC7E,KAAK,UAAU,gBAAgB;IAC7B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,4BAA4B,CAAC,cAAc,CAAC,CAAC;QAC/D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QAC1D,IAAI,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,GAAG,CAAC,EAAE,CAAC;YACpE,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,qCAAqC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,wEAAwE;AACxE,KAAK,UAAU,eAAe,CAAC,GAAW;IACxC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,4BAA4B,CAAC,cAAc,CAAC,CAAC;QAC/D,MAAM,KAAK,CAAC,GAAG,CAAC,aAAa,EAAE,aAAa,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,sCAAsC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjE,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,SAAS,eAAe;IACtB,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC;YAAE,OAAO,IAAI,CAAC;QAChD,MAAM,GAAG,GAAG,YAAY,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5D,IAAI,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,GAAG,CAAC,EAAE,CAAC;YACpE,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,+BAA+B,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,yEAAyE;AACzE,SAAS,cAAc,CAAC,GAAW;IACjC,IAAI,CAAC;QACH,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1D,aAAa,CAAC,iBAAiB,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,wDAAwD,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACrF,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CAAC,GAAY;IAC1B,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,4BAA4B;IAC1C,SAAS,GAAG,IAAI,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tamper-evident hash chain primitives (pure, node:crypto only).
|
|
3
|
+
*
|
|
4
|
+
* Every durable audit record is linked to its predecessor by a SHA-256 hash,
|
|
5
|
+
* forming an append-only chain. Any edit, deletion, reorder, or insertion
|
|
6
|
+
* breaks the chain at the point of tampering and is detectable by verifyChain().
|
|
7
|
+
*
|
|
8
|
+
* SCOPE — tamper EVIDENCE, not tamper PREVENTION. The optional HMAC binds the
|
|
9
|
+
* chain to a local key; an attacker who can READ that key can recompute a
|
|
10
|
+
* consistent chain. This module makes tampering DETECTABLE, never IMPOSSIBLE.
|
|
11
|
+
* Closing the same-user-key gap requires remote anchoring (see audit-chain.ts).
|
|
12
|
+
*
|
|
13
|
+
* @module @panguard-ai/panguard-guard/audit/hash-chain
|
|
14
|
+
*/
|
|
15
|
+
/** SHA-256 hex digest length. */
|
|
16
|
+
export declare const HASH_HEX_LENGTH = 64;
|
|
17
|
+
/** Genesis predecessor hash: 64 zeros. The first record in a chain links here. */
|
|
18
|
+
export declare const GENESIS_HASH: string;
|
|
19
|
+
/**
|
|
20
|
+
* A single chained record. The payload is the original log line (verbatim);
|
|
21
|
+
* seq/ts/prevHash/hash/hmac are the chain envelope. hash and hmac are EXCLUDED
|
|
22
|
+
* from the hashed material (a record cannot hash over its own hash).
|
|
23
|
+
*/
|
|
24
|
+
export interface ChainedRecord<T = unknown> {
|
|
25
|
+
/** Monotonic sequence, strictly incrementing from 0. */
|
|
26
|
+
readonly seq: number;
|
|
27
|
+
/** ISO timestamp when the record was chained. */
|
|
28
|
+
readonly ts: string;
|
|
29
|
+
/** The original, unmodified log payload. */
|
|
30
|
+
readonly payload: T;
|
|
31
|
+
/** Hash of the predecessor record (GENESIS_HASH for seq 0 or post-rotation genesis). */
|
|
32
|
+
readonly prevHash: string;
|
|
33
|
+
/** This record's hash (over prevHash, seq, ts, canonical payload). */
|
|
34
|
+
readonly hash: string;
|
|
35
|
+
/** Optional HMAC of `hash` under the local audit key. */
|
|
36
|
+
readonly hmac?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Why a chain failed verification (or 'ok'). */
|
|
39
|
+
export type VerifyReason = 'ok' | 'hash-break' | 'seq-gap' | 'bad-hmac' | 'truncated' | 'empty' | 'unchained-legacy';
|
|
40
|
+
/** Result of verifyChain over an ordered list of records. */
|
|
41
|
+
export interface VerifyResult {
|
|
42
|
+
/** True only when every chained record verified end-to-end. */
|
|
43
|
+
readonly ok: boolean;
|
|
44
|
+
/** Number of chained records that verified successfully. */
|
|
45
|
+
readonly verifiedCount: number;
|
|
46
|
+
/** Index (into the input array) of the first bad record, or -1 if none. */
|
|
47
|
+
readonly firstBadIndex: number;
|
|
48
|
+
/** Machine-readable reason. */
|
|
49
|
+
readonly reason: VerifyReason;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Deterministic JSON serialization with RECURSIVELY sorted object keys.
|
|
53
|
+
*
|
|
54
|
+
* Two semantically equal objects (same data, different key order, nested at any
|
|
55
|
+
* depth) serialize to the same string, so the hash is stable regardless of how
|
|
56
|
+
* the producer happened to order its keys. Arrays keep their order (order is
|
|
57
|
+
* semantically meaningful for arrays).
|
|
58
|
+
*
|
|
59
|
+
* CRITICAL: we first normalize through JSON (parse(stringify(value))) so values
|
|
60
|
+
* with a custom toJSON (Date, Buffer, etc.) and `undefined` fields are reduced to
|
|
61
|
+
* their PLAIN JSON form — exactly what a reader sees after JSON.parse. Without
|
|
62
|
+
* this, a Date in the payload would hash as `{}` at write time but as its ISO
|
|
63
|
+
* string at read time, silently breaking the chain. Normalizing guarantees
|
|
64
|
+
* write/read parity (the read side is already JSON-parsed).
|
|
65
|
+
*/
|
|
66
|
+
export declare function canonicalize(value: unknown): string;
|
|
67
|
+
/**
|
|
68
|
+
* Compute the chain hash for a record.
|
|
69
|
+
* hash = sha256( prevHash + '\n' + seq + '\n' + ts + '\n' + canonicalize(payload) )
|
|
70
|
+
*
|
|
71
|
+
* The newline delimiters make the field boundaries unambiguous so distinct
|
|
72
|
+
* field combinations cannot collide via concatenation.
|
|
73
|
+
*/
|
|
74
|
+
export declare function computeHash(prevHash: string, seq: number, ts: string, payload: unknown): string;
|
|
75
|
+
/**
|
|
76
|
+
* Sign a record hash with the local audit key.
|
|
77
|
+
* hmac = HMAC-SHA256(key, hash) hex.
|
|
78
|
+
*/
|
|
79
|
+
export declare function signHmac(hash: string, key: Buffer): string;
|
|
80
|
+
/**
|
|
81
|
+
* Verify an ordered list of records.
|
|
82
|
+
*
|
|
83
|
+
* Pre-existing un-chained legacy lines (no hash field) at the FRONT are treated
|
|
84
|
+
* as an un-verifiable prefix: verification starts at the first chained record
|
|
85
|
+
* and reports reason 'unchained-legacy' (verifiedCount counts the chained suffix
|
|
86
|
+
* only). This stops an upgrade from flagging the whole file as tampered.
|
|
87
|
+
*
|
|
88
|
+
* Walk rules over the chained suffix:
|
|
89
|
+
* - seq must strictly increment from the first chained record's seq baseline
|
|
90
|
+
* (else 'seq-gap'),
|
|
91
|
+
* - recompute hash and compare (else 'hash-break'),
|
|
92
|
+
* - prevHash of each record must equal the previous record's hash (else 'hash-break'),
|
|
93
|
+
* - if a key is supplied, recompute the HMAC and timing-safe compare (else 'bad-hmac').
|
|
94
|
+
*/
|
|
95
|
+
export declare function verifyChain(records: readonly unknown[], key?: Buffer): VerifyResult;
|
|
96
|
+
//# sourceMappingURL=hash-chain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash-chain.d.ts","sourceRoot":"","sources":["../../src/audit/hash-chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,iCAAiC;AACjC,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,kFAAkF;AAClF,eAAO,MAAM,YAAY,QAA8B,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACxC,wDAAwD;IACxD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,iDAAiD;AACjD,MAAM,MAAM,YAAY,GACpB,IAAI,GACJ,YAAY,GACZ,SAAS,GACT,UAAU,GACV,WAAW,GACX,OAAO,GACP,kBAAkB,CAAC;AAEvB,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,2EAA2E;IAC3E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,+BAA+B;IAC/B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOnD;AAsBD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,OAAO,GACf,MAAM,CAIR;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAuBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,YAAY,CAmEnF"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tamper-evident hash chain primitives (pure, node:crypto only).
|
|
3
|
+
*
|
|
4
|
+
* Every durable audit record is linked to its predecessor by a SHA-256 hash,
|
|
5
|
+
* forming an append-only chain. Any edit, deletion, reorder, or insertion
|
|
6
|
+
* breaks the chain at the point of tampering and is detectable by verifyChain().
|
|
7
|
+
*
|
|
8
|
+
* SCOPE — tamper EVIDENCE, not tamper PREVENTION. The optional HMAC binds the
|
|
9
|
+
* chain to a local key; an attacker who can READ that key can recompute a
|
|
10
|
+
* consistent chain. This module makes tampering DETECTABLE, never IMPOSSIBLE.
|
|
11
|
+
* Closing the same-user-key gap requires remote anchoring (see audit-chain.ts).
|
|
12
|
+
*
|
|
13
|
+
* @module @panguard-ai/panguard-guard/audit/hash-chain
|
|
14
|
+
*/
|
|
15
|
+
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
16
|
+
/** SHA-256 hex digest length. */
|
|
17
|
+
export const HASH_HEX_LENGTH = 64;
|
|
18
|
+
/** Genesis predecessor hash: 64 zeros. The first record in a chain links here. */
|
|
19
|
+
export const GENESIS_HASH = '0'.repeat(HASH_HEX_LENGTH);
|
|
20
|
+
/**
|
|
21
|
+
* Deterministic JSON serialization with RECURSIVELY sorted object keys.
|
|
22
|
+
*
|
|
23
|
+
* Two semantically equal objects (same data, different key order, nested at any
|
|
24
|
+
* depth) serialize to the same string, so the hash is stable regardless of how
|
|
25
|
+
* the producer happened to order its keys. Arrays keep their order (order is
|
|
26
|
+
* semantically meaningful for arrays).
|
|
27
|
+
*
|
|
28
|
+
* CRITICAL: we first normalize through JSON (parse(stringify(value))) so values
|
|
29
|
+
* with a custom toJSON (Date, Buffer, etc.) and `undefined` fields are reduced to
|
|
30
|
+
* their PLAIN JSON form — exactly what a reader sees after JSON.parse. Without
|
|
31
|
+
* this, a Date in the payload would hash as `{}` at write time but as its ISO
|
|
32
|
+
* string at read time, silently breaking the chain. Normalizing guarantees
|
|
33
|
+
* write/read parity (the read side is already JSON-parsed).
|
|
34
|
+
*/
|
|
35
|
+
export function canonicalize(value) {
|
|
36
|
+
// toJSONString handles undefined at the top level (JSON.stringify(undefined)
|
|
37
|
+
// is the string "undefined"'s absence — guard it).
|
|
38
|
+
const json = JSON.stringify(value);
|
|
39
|
+
if (json === undefined)
|
|
40
|
+
return JSON.stringify(null);
|
|
41
|
+
const normalized = JSON.parse(json);
|
|
42
|
+
return JSON.stringify(sortDeep(normalized));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Recursively rebuild a (already JSON-normalized) value with object keys sorted.
|
|
46
|
+
* Primitives pass through. Arrays recurse element-wise (order preserved). Objects
|
|
47
|
+
* are rebuilt key-sorted.
|
|
48
|
+
*/
|
|
49
|
+
function sortDeep(value) {
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
return value.map((item) => sortDeep(item));
|
|
52
|
+
}
|
|
53
|
+
if (value !== null && typeof value === 'object') {
|
|
54
|
+
const obj = value;
|
|
55
|
+
const sorted = {};
|
|
56
|
+
for (const key of Object.keys(obj).sort()) {
|
|
57
|
+
sorted[key] = sortDeep(obj[key]);
|
|
58
|
+
}
|
|
59
|
+
return sorted;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Compute the chain hash for a record.
|
|
65
|
+
* hash = sha256( prevHash + '\n' + seq + '\n' + ts + '\n' + canonicalize(payload) )
|
|
66
|
+
*
|
|
67
|
+
* The newline delimiters make the field boundaries unambiguous so distinct
|
|
68
|
+
* field combinations cannot collide via concatenation.
|
|
69
|
+
*/
|
|
70
|
+
export function computeHash(prevHash, seq, ts, payload) {
|
|
71
|
+
return createHash('sha256')
|
|
72
|
+
.update(`${prevHash}\n${seq}\n${ts}\n${canonicalize(payload)}`)
|
|
73
|
+
.digest('hex');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Sign a record hash with the local audit key.
|
|
77
|
+
* hmac = HMAC-SHA256(key, hash) hex.
|
|
78
|
+
*/
|
|
79
|
+
export function signHmac(hash, key) {
|
|
80
|
+
return createHmac('sha256', key).update(hash).digest('hex');
|
|
81
|
+
}
|
|
82
|
+
/** Constant-time hex-string compare. Length mismatch returns false (never throws). */
|
|
83
|
+
function timingSafeEqualHex(a, b) {
|
|
84
|
+
if (a.length !== b.length)
|
|
85
|
+
return false;
|
|
86
|
+
try {
|
|
87
|
+
return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** True if a record carries the chain envelope (a chained record, not a legacy line). */
|
|
94
|
+
function isChained(rec) {
|
|
95
|
+
if (rec === null || typeof rec !== 'object')
|
|
96
|
+
return false;
|
|
97
|
+
const r = rec;
|
|
98
|
+
return (typeof r['hash'] === 'string' &&
|
|
99
|
+
typeof r['prevHash'] === 'string' &&
|
|
100
|
+
typeof r['seq'] === 'number');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Verify an ordered list of records.
|
|
104
|
+
*
|
|
105
|
+
* Pre-existing un-chained legacy lines (no hash field) at the FRONT are treated
|
|
106
|
+
* as an un-verifiable prefix: verification starts at the first chained record
|
|
107
|
+
* and reports reason 'unchained-legacy' (verifiedCount counts the chained suffix
|
|
108
|
+
* only). This stops an upgrade from flagging the whole file as tampered.
|
|
109
|
+
*
|
|
110
|
+
* Walk rules over the chained suffix:
|
|
111
|
+
* - seq must strictly increment from the first chained record's seq baseline
|
|
112
|
+
* (else 'seq-gap'),
|
|
113
|
+
* - recompute hash and compare (else 'hash-break'),
|
|
114
|
+
* - prevHash of each record must equal the previous record's hash (else 'hash-break'),
|
|
115
|
+
* - if a key is supplied, recompute the HMAC and timing-safe compare (else 'bad-hmac').
|
|
116
|
+
*/
|
|
117
|
+
export function verifyChain(records, key) {
|
|
118
|
+
if (records.length === 0) {
|
|
119
|
+
return { ok: false, verifiedCount: 0, firstBadIndex: -1, reason: 'empty' };
|
|
120
|
+
}
|
|
121
|
+
// Find where the chained suffix begins. A legacy prefix is tolerated.
|
|
122
|
+
const firstChainedIndex = records.findIndex((r) => isChained(r));
|
|
123
|
+
if (firstChainedIndex === -1) {
|
|
124
|
+
// Entire file is legacy / un-chained — nothing verifiable, but not "tampered".
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
verifiedCount: 0,
|
|
128
|
+
firstBadIndex: -1,
|
|
129
|
+
reason: 'unchained-legacy',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const hasLegacyPrefix = firstChainedIndex > 0;
|
|
133
|
+
let verifiedCount = 0;
|
|
134
|
+
let expectedPrevHash = GENESIS_HASH;
|
|
135
|
+
let expectedSeq = records[firstChainedIndex].seq;
|
|
136
|
+
// The first chained record's seq is the baseline; subsequent records must
|
|
137
|
+
// increment by exactly 1. (A post-rotation genesis can start at any seq, but
|
|
138
|
+
// within a single readAll() the suffix is contiguous.)
|
|
139
|
+
for (let i = firstChainedIndex; i < records.length; i++) {
|
|
140
|
+
const rec = records[i];
|
|
141
|
+
if (!isChained(rec)) {
|
|
142
|
+
// A legacy line appearing AFTER chained records means the suffix was
|
|
143
|
+
// disturbed (e.g. a chained line replaced with a raw one) — hash-break.
|
|
144
|
+
return { ok: false, verifiedCount, firstBadIndex: i, reason: 'hash-break' };
|
|
145
|
+
}
|
|
146
|
+
if (rec.seq !== expectedSeq) {
|
|
147
|
+
return { ok: false, verifiedCount, firstBadIndex: i, reason: 'seq-gap' };
|
|
148
|
+
}
|
|
149
|
+
if (rec.prevHash !== expectedPrevHash) {
|
|
150
|
+
return { ok: false, verifiedCount, firstBadIndex: i, reason: 'hash-break' };
|
|
151
|
+
}
|
|
152
|
+
const recomputed = computeHash(rec.prevHash, rec.seq, rec.ts, rec.payload);
|
|
153
|
+
if (!timingSafeEqualHex(recomputed, rec.hash)) {
|
|
154
|
+
return { ok: false, verifiedCount, firstBadIndex: i, reason: 'hash-break' };
|
|
155
|
+
}
|
|
156
|
+
if (key) {
|
|
157
|
+
if (typeof rec.hmac !== 'string') {
|
|
158
|
+
return { ok: false, verifiedCount, firstBadIndex: i, reason: 'bad-hmac' };
|
|
159
|
+
}
|
|
160
|
+
const recomputedHmac = signHmac(rec.hash, key);
|
|
161
|
+
if (!timingSafeEqualHex(recomputedHmac, rec.hmac)) {
|
|
162
|
+
return { ok: false, verifiedCount, firstBadIndex: i, reason: 'bad-hmac' };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
verifiedCount++;
|
|
166
|
+
expectedPrevHash = rec.hash;
|
|
167
|
+
expectedSeq = rec.seq + 1;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: !hasLegacyPrefix,
|
|
171
|
+
verifiedCount,
|
|
172
|
+
firstBadIndex: -1,
|
|
173
|
+
reason: hasLegacyPrefix ? 'unchained-legacy' : 'ok',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=hash-chain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash-chain.js","sourceRoot":"","sources":["../../src/audit/hash-chain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tamper-evident audit subsystem.
|
|
3
|
+
*
|
|
4
|
+
* - hash-chain: pure SHA-256 chain + HMAC primitives and verifyChain.
|
|
5
|
+
* - audit-chain: durable, append-only, rotation-spanning JSONL chain.
|
|
6
|
+
* - audit-key: keychain-first / file-fallback HMAC key management.
|
|
7
|
+
* - attribution: forensic actor/decisionId/rule enrichment.
|
|
8
|
+
*
|
|
9
|
+
* @module @panguard-ai/panguard-guard/audit
|
|
10
|
+
*/
|
|
11
|
+
export { GENESIS_HASH, HASH_HEX_LENGTH, canonicalize, computeHash, signHmac, verifyChain, type ChainedRecord, type VerifyReason, type VerifyResult, } from './hash-chain.js';
|
|
12
|
+
export { AuditChain, type AuditChainOptions, type ChainHead, type AnchorHook, } from './audit-chain.js';
|
|
13
|
+
export { getAuditKey, __resetAuditKeyCacheForTests } from './audit-key.js';
|
|
14
|
+
export { buildActor, newDecisionId, anonymizeActorForExport, type Actor, type AgentAttribution, type RuleRef, } from './attribution.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE3E,OAAO,EACL,UAAU,EACV,aAAa,EACb,uBAAuB,EACvB,KAAK,KAAK,EACV,KAAK,gBAAgB,EACrB,KAAK,OAAO,GACb,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tamper-evident audit subsystem.
|
|
3
|
+
*
|
|
4
|
+
* - hash-chain: pure SHA-256 chain + HMAC primitives and verifyChain.
|
|
5
|
+
* - audit-chain: durable, append-only, rotation-spanning JSONL chain.
|
|
6
|
+
* - audit-key: keychain-first / file-fallback HMAC key management.
|
|
7
|
+
* - attribution: forensic actor/decisionId/rule enrichment.
|
|
8
|
+
*
|
|
9
|
+
* @module @panguard-ai/panguard-guard/audit
|
|
10
|
+
*/
|
|
11
|
+
export { GENESIS_HASH, HASH_HEX_LENGTH, canonicalize, computeHash, signHmac, verifyChain, } from './hash-chain.js';
|
|
12
|
+
export { AuditChain, } from './audit-chain.js';
|
|
13
|
+
export { getAuditKey, __resetAuditKeyCacheForTests } from './audit-key.js';
|
|
14
|
+
export { buildActor, newDecisionId, anonymizeActorForExport, } from './attribution.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,WAAW,GAIZ,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,UAAU,GAIX,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE3E,OAAO,EACL,UAAU,EACV,aAAa,EACb,uBAAuB,GAIxB,MAAM,kBAAkB,CAAC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -209,6 +209,33 @@ async function commandStart(dataDir, verbose = false, managerUrl, noTelemetry =
|
|
|
209
209
|
// Never let TC provisioning block or crash startup.
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
// S5: verify config integrity + self-state before the engine starts. On a genuine
|
|
213
|
+
// first run, establish the seal. On tamper / self-removal, start anyway but warn
|
|
214
|
+
// LOUDLY — never silently honor a weakened config as if PROTECTED (the S2 invariant).
|
|
215
|
+
try {
|
|
216
|
+
const { verifyConfigIntegrity, checkSelfState, sealConfigManifest, wasInitialized } = await import('../integrity.js');
|
|
217
|
+
const cfgRecord = config;
|
|
218
|
+
let verdict = verifyConfigIntegrity(cfgRecord, dataDir);
|
|
219
|
+
const selfState = checkSelfState(dataDir);
|
|
220
|
+
if (verdict.status === 'unsealed' && !wasInitialized(dataDir)) {
|
|
221
|
+
sealConfigManifest(cfgRecord, [], dataDir); // true first run — establish trust
|
|
222
|
+
verdict = { status: 'sealed', findings: [], checkedAt: verdict.checkedAt };
|
|
223
|
+
}
|
|
224
|
+
if (verdict.status !== 'sealed' || !selfState.ok) {
|
|
225
|
+
process.stderr.write(`\n[panguard-guard] INTEGRITY: ${verdict.status.toUpperCase()} — protection state may have been changed outside the guard.\n`);
|
|
226
|
+
// Field NAME only, never the value — config can hold secrets.
|
|
227
|
+
for (const f of verdict.findings) {
|
|
228
|
+
process.stderr.write(` - config.${f.field} changed (${f.severity})\n`);
|
|
229
|
+
}
|
|
230
|
+
for (const f of selfState.findings) {
|
|
231
|
+
process.stderr.write(` - ${f.kind} ${f.reason}${f.label ? ` (${f.label})` : ''}\n`);
|
|
232
|
+
}
|
|
233
|
+
process.stderr.write(' Run "pga doctor" to review. If you made this change, re-run "pga up" to re-seal.\n\n');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
/* integrity is best-effort — never block daemon startup (consistent with TC provisioning) */
|
|
238
|
+
}
|
|
212
239
|
const engine = await GuardEngine.create(config);
|
|
213
240
|
const shutdown = async () => {
|
|
214
241
|
console.log(`\n ${symbols.info} Shutting down PanguardGuard...`);
|