@panguard-ai/panguard-guard 1.7.1 → 1.7.3
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/dashboard.html +10 -10
- 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/dist/threat-cloud/seal.d.ts +66 -0
- package/dist/threat-cloud/seal.d.ts.map +1 -0
- package/dist/threat-cloud/seal.js +77 -0
- package/dist/threat-cloud/seal.js.map +1 -0
- package/package.json +12 -8
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forensic attribution for audit records.
|
|
3
|
+
*
|
|
4
|
+
* Every durable audit payload is enriched with a stable `actor` block (who/where
|
|
5
|
+
* the record was produced) plus a `decisionId` and lifted `rule` identity, so an
|
|
6
|
+
* auditor can answer "who did this, on what host, under which rule" from the log
|
|
7
|
+
* alone.
|
|
8
|
+
*
|
|
9
|
+
* The FULL actor (including OS username) is kept in the LOCAL durable log. The
|
|
10
|
+
* EXPORT path anonymizes the username before forwarding to an external auditor
|
|
11
|
+
* (see anonymizeActorForExport).
|
|
12
|
+
*
|
|
13
|
+
* @module @panguard-ai/panguard-guard/audit/attribution
|
|
14
|
+
*/
|
|
15
|
+
import { hostname, userInfo } from 'node:os';
|
|
16
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
17
|
+
/**
|
|
18
|
+
* Build the local (full) actor block for the current process. Pass agent identity
|
|
19
|
+
* when the record originates from a guarded agent session.
|
|
20
|
+
*/
|
|
21
|
+
export function buildActor(agent) {
|
|
22
|
+
return {
|
|
23
|
+
user: safeUsername(),
|
|
24
|
+
host: safeHostname(),
|
|
25
|
+
pid: process.pid,
|
|
26
|
+
...(agent ? { agent } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Generate a fresh decision id. */
|
|
30
|
+
export function newDecisionId() {
|
|
31
|
+
return `dec-${randomUUID()}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Anonymize an actor for EXPORT: replace the username with a stable salted hash
|
|
35
|
+
* so an auditor can correlate same-user records without learning the username,
|
|
36
|
+
* and never leak a real OS username into a forwarded document. Host/pid/agent
|
|
37
|
+
* are retained (operationally useful, not personally identifying in the same way).
|
|
38
|
+
*/
|
|
39
|
+
export function anonymizeActorForExport(actor) {
|
|
40
|
+
if (!actor)
|
|
41
|
+
return undefined;
|
|
42
|
+
return {
|
|
43
|
+
user_hash: hashUser(actor.user),
|
|
44
|
+
host: actor.host,
|
|
45
|
+
pid: actor.pid,
|
|
46
|
+
...(actor.agent ? { agent: actor.agent } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** SHA-256 of the username with a fixed domain separator. Not reversible. */
|
|
50
|
+
function hashUser(user) {
|
|
51
|
+
return createHash('sha256').update(`panguard-audit-user:${user}`).digest('hex').slice(0, 32);
|
|
52
|
+
}
|
|
53
|
+
function safeUsername() {
|
|
54
|
+
try {
|
|
55
|
+
return userInfo().username;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return 'unknown';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function safeHostname() {
|
|
62
|
+
try {
|
|
63
|
+
return hostname();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=attribution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../../src/audit/attribution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2BrD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAwB;IACjD,OAAO;QACL,IAAI,EAAE,YAAY,EAAE;QACpB,IAAI,EAAE,YAAY,EAAE;QACpB,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5B,CAAC;AACJ,CAAC;AAED,oCAAoC;AACpC,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,UAAU,EAAE,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAwB;IAC9D,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,OAAO;QACL,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;QAC/B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/C,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC/F,CAAC;AAED,SAAS,YAAY;IACnB,IAAI,CAAC;QACH,OAAO,QAAQ,EAAE,CAAC,QAAQ,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,YAAY;IACnB,IAAI,CAAC;QACH,OAAO,QAAQ,EAAE,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuditChain — durable, append-only, tamper-evident JSONL log.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a JSONL file so every appended line is a {@link ChainedRecord}: linked
|
|
5
|
+
* to its predecessor by a SHA-256 hash and signed with a local HMAC key. The
|
|
6
|
+
* chain head (last seq + last hash) is persisted to a rotation-independent
|
|
7
|
+
* head-anchor so:
|
|
8
|
+
* - a fresh process can append without rescanning the whole file, and
|
|
9
|
+
* - tail truncation is detectable (head-anchor seq > last record seq).
|
|
10
|
+
*
|
|
11
|
+
* The chain SPANS rotation: when the active log is rotated out, the next file's
|
|
12
|
+
* first record links to the rotated-out file's last hash (carried in the
|
|
13
|
+
* head-anchor), so the chain is continuous across events.jsonl + events.jsonl.1.
|
|
14
|
+
*
|
|
15
|
+
* SCOPE — tamper EVIDENCE. The HMAC key is same-user-readable (see audit-key.ts
|
|
16
|
+
* and SECURITY.md). This detects tampering; it does not prevent it. The no-op
|
|
17
|
+
* anchor seam (getHead + optional anchor hook) is where remote anchoring — the
|
|
18
|
+
* enterprise upgrade that closes the same-user gap — plugs in.
|
|
19
|
+
*
|
|
20
|
+
* @module @panguard-ai/panguard-guard/audit/audit-chain
|
|
21
|
+
*/
|
|
22
|
+
import { type ChainedRecord, type VerifyResult } from './hash-chain.js';
|
|
23
|
+
/** Persisted chain head — the anchor for appends and truncation detection. */
|
|
24
|
+
export interface ChainHead {
|
|
25
|
+
/** seq of the last appended record (-1 if the chain is empty). */
|
|
26
|
+
readonly seq: number;
|
|
27
|
+
/** hash of the last appended record (GENESIS_HASH if empty). */
|
|
28
|
+
readonly hash: string;
|
|
29
|
+
/** ISO timestamp the head was last updated. */
|
|
30
|
+
readonly updatedAt: string;
|
|
31
|
+
}
|
|
32
|
+
/** Hook invoked after the head advances. Default is a no-op (local-only). */
|
|
33
|
+
export type AnchorHook = (head: ChainHead) => void;
|
|
34
|
+
/** Options for the audit chain. */
|
|
35
|
+
export interface AuditChainOptions {
|
|
36
|
+
/** HMAC signing key. When omitted, records are hashed but not HMAC-signed. */
|
|
37
|
+
readonly key?: Buffer;
|
|
38
|
+
/**
|
|
39
|
+
* Directory holding the rotation-independent head-anchor. Defaults to the log
|
|
40
|
+
* file's directory. The anchor file itself is `<dataDir>/chainHead.json` when
|
|
41
|
+
* dataDir is given, else `<logPath>.head`.
|
|
42
|
+
*/
|
|
43
|
+
readonly dataDir?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Remote-anchor seam. Called (best-effort) after each head advance with the
|
|
46
|
+
* new head. Default no-op keeps the chain local-only. An enterprise build can
|
|
47
|
+
* publish the head to Threat Cloud here to close the same-user-key gap.
|
|
48
|
+
*/
|
|
49
|
+
readonly anchor?: AnchorHook;
|
|
50
|
+
}
|
|
51
|
+
/** Append-only tamper-evident JSONL chain over a single logical log file. */
|
|
52
|
+
export declare class AuditChain {
|
|
53
|
+
private readonly logPath;
|
|
54
|
+
private readonly headPath;
|
|
55
|
+
private readonly key;
|
|
56
|
+
private readonly anchor;
|
|
57
|
+
private head;
|
|
58
|
+
constructor(logPath: string, opts?: AuditChainOptions);
|
|
59
|
+
/**
|
|
60
|
+
* Append a payload as a new chained record and persist it. Returns the record.
|
|
61
|
+
*
|
|
62
|
+
* Fail-open on WRITE error: a broken audit file must never brick the guarded
|
|
63
|
+
* path. Append errors log loudly to stderr; the record is still returned so
|
|
64
|
+
* the caller proceeds.
|
|
65
|
+
*/
|
|
66
|
+
append<T>(payload: T): ChainedRecord<T>;
|
|
67
|
+
/**
|
|
68
|
+
* Read every record across the active file + rotated files, oldest first,
|
|
69
|
+
* using a streaming reader (line-by-line, memory-bounded). Chained lines parse
|
|
70
|
+
* to ChainedRecord; legacy un-chained lines are preserved verbatim as their
|
|
71
|
+
* parsed object so verify() can classify them as the 'unchained-legacy' prefix.
|
|
72
|
+
* The element type is `unknown` because the suffix may mix both (callers cast
|
|
73
|
+
* + filter; verify() accepts unknown[]).
|
|
74
|
+
*/
|
|
75
|
+
readAll(): Promise<ChainedRecord[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Verify the durable chain end-to-end. Detects edits, deletions, insertions,
|
|
78
|
+
* reorders (all 'hash-break'/'seq-gap'), bad HMAC, and tail truncation
|
|
79
|
+
* (head-anchor seq strictly greater than the last on-disk record seq).
|
|
80
|
+
*/
|
|
81
|
+
verify(): Promise<VerifyResult>;
|
|
82
|
+
/** Current persisted chain head. The no-op anchor seam reads this. */
|
|
83
|
+
getHead(): ChainHead;
|
|
84
|
+
/**
|
|
85
|
+
* Carry the chain across a log rotation. The caller (ReportAgent) renames the
|
|
86
|
+
* active file out of the way; the head-anchor already holds the rotated-out
|
|
87
|
+
* file's last hash, so the NEXT append's prevHash chains to it automatically.
|
|
88
|
+
* This is a no-op marker for clarity + future hooks — the head is the source
|
|
89
|
+
* of truth, so rotation needs no special chain handling here.
|
|
90
|
+
*/
|
|
91
|
+
noteRotation(): void;
|
|
92
|
+
private writeHead;
|
|
93
|
+
/**
|
|
94
|
+
* Load the head-anchor. If it is missing or stale (file exists but anchor
|
|
95
|
+
* absent), self-heal by streaming the file to recompute the true head and
|
|
96
|
+
* warn loudly.
|
|
97
|
+
*/
|
|
98
|
+
private loadOrHealHead;
|
|
99
|
+
private tryReadHead;
|
|
100
|
+
/** Synchronous head recompute from the last on-disk record (self-heal path). */
|
|
101
|
+
private recomputeHeadSync;
|
|
102
|
+
private safeAnchor;
|
|
103
|
+
/** Active file first, then rotated files in OLDEST-first order? No: we want
|
|
104
|
+
* oldest-first overall so the chain reads in append order. Rotation produces
|
|
105
|
+
* events.jsonl (newest) + events.jsonl.1 (older) + ... so oldest is highest N. */
|
|
106
|
+
private orderedLogFiles;
|
|
107
|
+
private rotatedFiles;
|
|
108
|
+
private streamFile;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=audit-chain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-chain.d.ts","sourceRoot":"","sources":["../../src/audit/audit-chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAeH,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAC;AAIzB,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;AAEnD,mCAAmC;AACnC,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;CAC9B;AAID,6EAA6E;AAC7E,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,IAAI,CAAY;gBAEZ,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB;IAiBzD;;;;;;OAMG;IACH,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;IA8BvC;;;;;;;OAOG;IACG,OAAO,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IASzC;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IA6BrC,sEAAsE;IACtE,OAAO,IAAI,SAAS;IAQpB;;;;;;OAMG;IACH,YAAY,IAAI,IAAI;IAUpB,OAAO,CAAC,SAAS;IAMjB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAkCtB,OAAO,CAAC,WAAW;IAkBnB,gFAAgF;IAChF,OAAO,CAAC,iBAAiB;IAwBzB,OAAO,CAAC,UAAU;IAalB;;sFAEkF;IAClF,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,UAAU;CAuBnB"}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuditChain — durable, append-only, tamper-evident JSONL log.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a JSONL file so every appended line is a {@link ChainedRecord}: linked
|
|
5
|
+
* to its predecessor by a SHA-256 hash and signed with a local HMAC key. The
|
|
6
|
+
* chain head (last seq + last hash) is persisted to a rotation-independent
|
|
7
|
+
* head-anchor so:
|
|
8
|
+
* - a fresh process can append without rescanning the whole file, and
|
|
9
|
+
* - tail truncation is detectable (head-anchor seq > last record seq).
|
|
10
|
+
*
|
|
11
|
+
* The chain SPANS rotation: when the active log is rotated out, the next file's
|
|
12
|
+
* first record links to the rotated-out file's last hash (carried in the
|
|
13
|
+
* head-anchor), so the chain is continuous across events.jsonl + events.jsonl.1.
|
|
14
|
+
*
|
|
15
|
+
* SCOPE — tamper EVIDENCE. The HMAC key is same-user-readable (see audit-key.ts
|
|
16
|
+
* and SECURITY.md). This detects tampering; it does not prevent it. The no-op
|
|
17
|
+
* anchor seam (getHead + optional anchor hook) is where remote anchoring — the
|
|
18
|
+
* enterprise upgrade that closes the same-user gap — plugs in.
|
|
19
|
+
*
|
|
20
|
+
* @module @panguard-ai/panguard-guard/audit/audit-chain
|
|
21
|
+
*/
|
|
22
|
+
import { appendFileSync, createReadStream, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
23
|
+
import { basename, dirname, join } from 'node:path';
|
|
24
|
+
import { createInterface } from 'node:readline';
|
|
25
|
+
import { createLogger } from '@panguard-ai/core';
|
|
26
|
+
import { GENESIS_HASH, computeHash, signHmac, verifyChain, } from './hash-chain.js';
|
|
27
|
+
const logger = createLogger('panguard-guard:audit-chain');
|
|
28
|
+
const EMPTY_HEAD = { seq: -1, hash: GENESIS_HASH, updatedAt: '' };
|
|
29
|
+
/** Append-only tamper-evident JSONL chain over a single logical log file. */
|
|
30
|
+
export class AuditChain {
|
|
31
|
+
logPath;
|
|
32
|
+
headPath;
|
|
33
|
+
key;
|
|
34
|
+
anchor;
|
|
35
|
+
head;
|
|
36
|
+
constructor(logPath, opts = {}) {
|
|
37
|
+
this.logPath = logPath;
|
|
38
|
+
this.key = opts.key;
|
|
39
|
+
this.anchor = opts.anchor ?? (() => { });
|
|
40
|
+
this.headPath = opts.dataDir
|
|
41
|
+
? join(opts.dataDir, 'chainHead.json')
|
|
42
|
+
: `${logPath}.head`;
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(dirname(logPath), { recursive: true, mode: 0o700 });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Directory may already exist.
|
|
48
|
+
}
|
|
49
|
+
this.head = this.loadOrHealHead();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Append a payload as a new chained record and persist it. Returns the record.
|
|
53
|
+
*
|
|
54
|
+
* Fail-open on WRITE error: a broken audit file must never brick the guarded
|
|
55
|
+
* path. Append errors log loudly to stderr; the record is still returned so
|
|
56
|
+
* the caller proceeds.
|
|
57
|
+
*/
|
|
58
|
+
append(payload) {
|
|
59
|
+
const seq = this.head.seq + 1;
|
|
60
|
+
const ts = new Date().toISOString();
|
|
61
|
+
const prevHash = this.head.hash;
|
|
62
|
+
const hash = computeHash(prevHash, seq, ts, payload);
|
|
63
|
+
const record = {
|
|
64
|
+
seq,
|
|
65
|
+
ts,
|
|
66
|
+
payload,
|
|
67
|
+
prevHash,
|
|
68
|
+
hash,
|
|
69
|
+
...(this.key ? { hmac: signHmac(hash, this.key) } : {}),
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
appendFileSync(this.logPath, JSON.stringify(record) + '\n', 'utf-8');
|
|
73
|
+
const nextHead = { seq, hash, updatedAt: ts };
|
|
74
|
+
this.writeHead(nextHead);
|
|
75
|
+
this.head = nextHead;
|
|
76
|
+
this.safeAnchor(nextHead);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
// FAIL-OPEN on write: loud stderr, but the caller proceeds.
|
|
80
|
+
process.stderr.write(`[panguard-audit] append failed (continuing fail-open): ${errMsg(err)}\n`);
|
|
81
|
+
}
|
|
82
|
+
return record;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read every record across the active file + rotated files, oldest first,
|
|
86
|
+
* using a streaming reader (line-by-line, memory-bounded). Chained lines parse
|
|
87
|
+
* to ChainedRecord; legacy un-chained lines are preserved verbatim as their
|
|
88
|
+
* parsed object so verify() can classify them as the 'unchained-legacy' prefix.
|
|
89
|
+
* The element type is `unknown` because the suffix may mix both (callers cast
|
|
90
|
+
* + filter; verify() accepts unknown[]).
|
|
91
|
+
*/
|
|
92
|
+
async readAll() {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const filePath of this.orderedLogFiles()) {
|
|
95
|
+
const recs = await this.streamFile(filePath);
|
|
96
|
+
out.push(...recs);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Verify the durable chain end-to-end. Detects edits, deletions, insertions,
|
|
102
|
+
* reorders (all 'hash-break'/'seq-gap'), bad HMAC, and tail truncation
|
|
103
|
+
* (head-anchor seq strictly greater than the last on-disk record seq).
|
|
104
|
+
*/
|
|
105
|
+
async verify() {
|
|
106
|
+
const records = await this.readAll();
|
|
107
|
+
const base = verifyChain(records, this.key);
|
|
108
|
+
// A content-level failure (hash-break / seq-gap / bad-hmac) is MORE specific
|
|
109
|
+
// than truncation and points at the exact bad index, so it wins. Truncation
|
|
110
|
+
// is reported only when the surviving content otherwise verifies — i.e. the
|
|
111
|
+
// chain is intact but shorter than the head-anchor remembers (tail removed).
|
|
112
|
+
const contentOk = base.ok || base.reason === 'unchained-legacy';
|
|
113
|
+
if (!contentOk)
|
|
114
|
+
return base;
|
|
115
|
+
// Truncation: the head-anchor remembers a seq the file no longer contains.
|
|
116
|
+
// Use the highest CHAINED seq on disk (legacy lines have no seq).
|
|
117
|
+
let lastSeq = -1;
|
|
118
|
+
for (const rec of records) {
|
|
119
|
+
if (isChainedRecord(rec) && rec.seq > lastSeq)
|
|
120
|
+
lastSeq = rec.seq;
|
|
121
|
+
}
|
|
122
|
+
if (this.head.seq > lastSeq) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
verifiedCount: base.verifiedCount,
|
|
126
|
+
firstBadIndex: records.length,
|
|
127
|
+
reason: 'truncated',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return base;
|
|
131
|
+
}
|
|
132
|
+
/** Current persisted chain head. The no-op anchor seam reads this. */
|
|
133
|
+
getHead() {
|
|
134
|
+
return this.head;
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Rotation support — kept rotation-independent via the head-anchor.
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
/**
|
|
140
|
+
* Carry the chain across a log rotation. The caller (ReportAgent) renames the
|
|
141
|
+
* active file out of the way; the head-anchor already holds the rotated-out
|
|
142
|
+
* file's last hash, so the NEXT append's prevHash chains to it automatically.
|
|
143
|
+
* This is a no-op marker for clarity + future hooks — the head is the source
|
|
144
|
+
* of truth, so rotation needs no special chain handling here.
|
|
145
|
+
*/
|
|
146
|
+
noteRotation() {
|
|
147
|
+
logger.info(`Audit chain spans rotation at seq ${this.head.seq} (head hash carried forward)`);
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Head-anchor persistence (tmp + rename, 0600).
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
writeHead(head) {
|
|
153
|
+
const tmp = `${this.headPath}.tmp`;
|
|
154
|
+
writeFileSync(tmp, JSON.stringify(head), { mode: 0o600 });
|
|
155
|
+
renameSync(tmp, this.headPath);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Load the head-anchor. If it is missing or stale (file exists but anchor
|
|
159
|
+
* absent), self-heal by streaming the file to recompute the true head and
|
|
160
|
+
* warn loudly.
|
|
161
|
+
*/
|
|
162
|
+
loadOrHealHead() {
|
|
163
|
+
const onDisk = this.tryReadHead();
|
|
164
|
+
const fileExists = existsSync(this.logPath) || this.rotatedFiles().length > 0;
|
|
165
|
+
if (onDisk && !fileExists) {
|
|
166
|
+
// Anchor present but no log — treat anchor as authoritative (empty chain
|
|
167
|
+
// is the only safe assumption if files vanished; keep anchor for truncation
|
|
168
|
+
// detection on next verify()).
|
|
169
|
+
return onDisk;
|
|
170
|
+
}
|
|
171
|
+
if (!onDisk && !fileExists) {
|
|
172
|
+
return EMPTY_HEAD;
|
|
173
|
+
}
|
|
174
|
+
if (!onDisk && fileExists) {
|
|
175
|
+
// Self-heal: recompute head from the file.
|
|
176
|
+
logger.warn(`Audit head-anchor missing but log exists at ${this.logPath} — self-healing by stream`);
|
|
177
|
+
const healed = this.recomputeHeadSync();
|
|
178
|
+
try {
|
|
179
|
+
this.writeHead(healed);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
logger.warn(`Failed to persist healed head: ${errMsg(err)}`);
|
|
183
|
+
}
|
|
184
|
+
return healed;
|
|
185
|
+
}
|
|
186
|
+
// Both present — anchor is authoritative for appends. Truncation/edit is
|
|
187
|
+
// surfaced by verify(), not by silently trusting or rewriting here.
|
|
188
|
+
return onDisk;
|
|
189
|
+
}
|
|
190
|
+
tryReadHead() {
|
|
191
|
+
try {
|
|
192
|
+
if (!existsSync(this.headPath))
|
|
193
|
+
return null;
|
|
194
|
+
const raw = readFileSync(this.headPath, 'utf-8');
|
|
195
|
+
const parsed = JSON.parse(raw);
|
|
196
|
+
if (typeof parsed.seq === 'number' && typeof parsed.hash === 'string') {
|
|
197
|
+
return {
|
|
198
|
+
seq: parsed.seq,
|
|
199
|
+
hash: parsed.hash,
|
|
200
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
logger.warn(`Audit head-anchor unreadable: ${errMsg(err)}`);
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
/** Synchronous head recompute from the last on-disk record (self-heal path). */
|
|
210
|
+
recomputeHeadSync() {
|
|
211
|
+
let lastChained = null;
|
|
212
|
+
for (const filePath of this.orderedLogFiles()) {
|
|
213
|
+
try {
|
|
214
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
215
|
+
const lines = content.split('\n').filter((l) => l.trim().length > 0);
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
const parsed = safeParse(line);
|
|
218
|
+
if (parsed && isChainedRecord(parsed)) {
|
|
219
|
+
lastChained = parsed;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// skip unreadable file
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!lastChained)
|
|
228
|
+
return EMPTY_HEAD;
|
|
229
|
+
return {
|
|
230
|
+
seq: lastChained.seq,
|
|
231
|
+
hash: lastChained.hash,
|
|
232
|
+
updatedAt: lastChained.ts,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
safeAnchor(head) {
|
|
236
|
+
try {
|
|
237
|
+
this.anchor(head);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
// Anchoring is best-effort: never let it break the append path.
|
|
241
|
+
process.stderr.write(`[panguard-audit] anchor hook failed: ${errMsg(err)}\n`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// File discovery + streaming (mirrors report-agent's stream pattern).
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
/** Active file first, then rotated files in OLDEST-first order? No: we want
|
|
248
|
+
* oldest-first overall so the chain reads in append order. Rotation produces
|
|
249
|
+
* events.jsonl (newest) + events.jsonl.1 (older) + ... so oldest is highest N. */
|
|
250
|
+
orderedLogFiles() {
|
|
251
|
+
const rotated = this.rotatedFiles(); // ascending by N: .1, .2, ...
|
|
252
|
+
// Oldest first = highest N first, then the active file last.
|
|
253
|
+
const oldestFirst = [...rotated].reverse().map((r) => r.file);
|
|
254
|
+
return [...oldestFirst, this.logPath];
|
|
255
|
+
}
|
|
256
|
+
rotatedFiles() {
|
|
257
|
+
const dir = dirname(this.logPath);
|
|
258
|
+
const base = basename(this.logPath);
|
|
259
|
+
try {
|
|
260
|
+
return readdirSync(dir)
|
|
261
|
+
.filter((f) => f.startsWith(base + '.'))
|
|
262
|
+
.map((f) => ({ file: join(dir, f), num: parseInt(f.slice(base.length + 1), 10) }))
|
|
263
|
+
.filter((x) => !isNaN(x.num))
|
|
264
|
+
.sort((a, b) => a.num - b.num);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
streamFile(filePath) {
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
const records = [];
|
|
273
|
+
try {
|
|
274
|
+
if (!existsSync(filePath)) {
|
|
275
|
+
resolve(records);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const stream = createReadStream(filePath, { encoding: 'utf-8' });
|
|
279
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
280
|
+
rl.on('line', (line) => {
|
|
281
|
+
if (!line.trim())
|
|
282
|
+
return;
|
|
283
|
+
const parsed = safeParse(line);
|
|
284
|
+
// Push chained AND legacy lines verbatim; verify() classifies them.
|
|
285
|
+
if (parsed)
|
|
286
|
+
records.push(parsed);
|
|
287
|
+
});
|
|
288
|
+
rl.on('close', () => resolve(records));
|
|
289
|
+
rl.on('error', () => resolve(records));
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
resolve(records);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function safeParse(line) {
|
|
298
|
+
try {
|
|
299
|
+
return JSON.parse(line);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function isChainedRecord(rec) {
|
|
306
|
+
if (rec === null || typeof rec !== 'object')
|
|
307
|
+
return false;
|
|
308
|
+
const r = rec;
|
|
309
|
+
return (typeof r['hash'] === 'string' &&
|
|
310
|
+
typeof r['prevHash'] === 'string' &&
|
|
311
|
+
typeof r['seq'] === 'number');
|
|
312
|
+
}
|
|
313
|
+
function errMsg(err) {
|
|
314
|
+
return err instanceof Error ? err.message : String(err);
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=audit-chain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-chain.js","sourceRoot":"","sources":["../../src/audit/audit-chain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Get (or generate-once and persist) the audit HMAC key.
|
|
19
|
+
*
|
|
20
|
+
* Never throws: if the keychain tier is unavailable it falls through to the
|
|
21
|
+
* file fallback; if even the file cannot be persisted, an in-memory key is
|
|
22
|
+
* returned so audit signing still works for the life of the process.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getAuditKey(): Promise<Buffer>;
|
|
25
|
+
/** Reset the in-process cache. Test-only seam. */
|
|
26
|
+
export declare function __resetAuditKeyCacheForTests(): void;
|
|
27
|
+
//# sourceMappingURL=audit-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-key.d.ts","sourceRoot":"","sources":["../../src/audit/audit-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAiCH;;;;;;GAMG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAyBnD;AAwDD,kDAAkD;AAClD,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD"}
|