sealkeep 0.11.5 → 0.11.7
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/CHANGELOG.md +53 -0
- package/CONTROL_PLANE.md +2 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/chunk-store.js +104 -82
- package/dist/src/cli.js +150 -5
- package/dist/src/control-plane/auth.d.ts +9 -0
- package/dist/src/control-plane/auth.js +19 -2
- package/dist/src/control-plane/server.js +47 -0
- package/dist/src/control-plane/store.d.ts +32 -0
- package/dist/src/control-plane/store.js +33 -0
- package/dist/src/daemon.js +2 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/leakscan.d.ts +101 -9
- package/dist/src/leakscan.js +481 -23
- package/dist/src/local-api.js +16 -33
- package/dist/src/machine-settings.d.ts +26 -0
- package/dist/src/machine-settings.js +23 -0
- package/dist/src/queue.d.ts +11 -1
- package/dist/src/queue.js +2 -2
- package/dist/src/stream-to-cloud.d.ts +2 -0
- package/dist/src/stream-to-cloud.js +80 -59
- package/dist/src/types.d.ts +18 -0
- package/dist/src/vault.js +38 -8
- package/docs/README.md +21 -0
- package/docs/secret-scanning.md +81 -0
- package/package.json +3 -2
- package/web/app.js +24 -6
|
@@ -66,6 +66,31 @@ export type AuditEvent = {
|
|
|
66
66
|
outcome: "allowed" | "denied";
|
|
67
67
|
detail?: Record<string, string | number>;
|
|
68
68
|
};
|
|
69
|
+
export type SecretScanCount = {
|
|
70
|
+
kind: string;
|
|
71
|
+
severity: "high" | "medium";
|
|
72
|
+
count: number;
|
|
73
|
+
};
|
|
74
|
+
export type SecretScanRollup = {
|
|
75
|
+
id: string;
|
|
76
|
+
accountId: string;
|
|
77
|
+
deviceId: string;
|
|
78
|
+
archiveId: string;
|
|
79
|
+
scannedAt: string;
|
|
80
|
+
receivedAt: string;
|
|
81
|
+
counts: SecretScanCount[];
|
|
82
|
+
};
|
|
83
|
+
export type SecretScanPolicy = {
|
|
84
|
+
accountId: string;
|
|
85
|
+
updatedAt: string;
|
|
86
|
+
mode: "off" | "record" | "block-high";
|
|
87
|
+
failClosed: boolean;
|
|
88
|
+
patterns: Array<{
|
|
89
|
+
kind: string;
|
|
90
|
+
severity: "high" | "medium";
|
|
91
|
+
source: string;
|
|
92
|
+
}>;
|
|
93
|
+
};
|
|
69
94
|
export declare class ControlPlaneStore {
|
|
70
95
|
private readonly path?;
|
|
71
96
|
private document;
|
|
@@ -98,4 +123,11 @@ export declare class ControlPlaneStore {
|
|
|
98
123
|
at?: string;
|
|
99
124
|
}): AuditEvent;
|
|
100
125
|
usedBytes(accountId: string): number;
|
|
126
|
+
/** 90 days. Rollups are counts only. Reads and writes both drop expired rows. */
|
|
127
|
+
static readonly ROLLUP_RETENTION_MS: number;
|
|
128
|
+
private freshRollups;
|
|
129
|
+
secretScanPolicy(accountId: string): SecretScanPolicy | undefined;
|
|
130
|
+
setSecretScanPolicy(policy: SecretScanPolicy): SecretScanPolicy;
|
|
131
|
+
addSecretScanRollup(rollup: SecretScanRollup, now?: number): SecretScanRollup;
|
|
132
|
+
secretScanRollups(accountId: string, now?: number): SecretScanRollup[];
|
|
101
133
|
}
|
|
@@ -79,4 +79,37 @@ export class ControlPlaneStore {
|
|
|
79
79
|
return entry;
|
|
80
80
|
}
|
|
81
81
|
usedBytes(accountId) { return this.archivesFor(accountId).filter((item) => item.durableAt).reduce((total, item) => total + item.bytes, 0); }
|
|
82
|
+
/** 90 days. Rollups are counts only. Reads and writes both drop expired rows. */
|
|
83
|
+
static ROLLUP_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
84
|
+
freshRollups(now = Date.now()) {
|
|
85
|
+
const all = this.document.secretScanRollups ?? [];
|
|
86
|
+
const fresh = all.filter((item) => now - Date.parse(item.receivedAt) <= ControlPlaneStore.ROLLUP_RETENTION_MS);
|
|
87
|
+
if (fresh.length !== all.length) {
|
|
88
|
+
this.document.secretScanRollups = fresh;
|
|
89
|
+
this.persist();
|
|
90
|
+
}
|
|
91
|
+
return fresh;
|
|
92
|
+
}
|
|
93
|
+
secretScanPolicy(accountId) { return (this.document.secretScanPolicies ?? []).find((item) => item.accountId === accountId); }
|
|
94
|
+
setSecretScanPolicy(policy) {
|
|
95
|
+
const policies = this.document.secretScanPolicies ?? [];
|
|
96
|
+
const index = policies.findIndex((item) => item.accountId === policy.accountId);
|
|
97
|
+
if (index >= 0)
|
|
98
|
+
policies[index] = policy;
|
|
99
|
+
else
|
|
100
|
+
policies.push(policy);
|
|
101
|
+
this.document.secretScanPolicies = policies;
|
|
102
|
+
this.persist();
|
|
103
|
+
return policy;
|
|
104
|
+
}
|
|
105
|
+
addSecretScanRollup(rollup, now = Date.now()) {
|
|
106
|
+
const kept = this.freshRollups(now);
|
|
107
|
+
kept.push(rollup);
|
|
108
|
+
this.document.secretScanRollups = kept;
|
|
109
|
+
this.persist();
|
|
110
|
+
return rollup;
|
|
111
|
+
}
|
|
112
|
+
secretScanRollups(accountId, now = Date.now()) {
|
|
113
|
+
return this.freshRollups(now).filter((item) => item.accountId === accountId);
|
|
114
|
+
}
|
|
82
115
|
}
|
package/dist/src/daemon.js
CHANGED
|
@@ -16,6 +16,7 @@ import { automaticTranscriptIsEnabled, backgroundUploadBytesPerSecond, readLocal
|
|
|
16
16
|
import { hasDueAutomaticAgentContextRequest, recoverAutomaticAgentContextClaims, } from "./agent-context.js";
|
|
17
17
|
import { reconcileTeamHistoryBackfill } from "./team-backfill.js";
|
|
18
18
|
import { acquireDaemonWorkerLease } from "./daemon-lease.js";
|
|
19
|
+
import { sweepStaleScanSnapshots } from "./leakscan.js";
|
|
19
20
|
import { createProgressDeadline } from "./progress-deadline.js";
|
|
20
21
|
import { recoverBackgroundBandwidthLockAtDaemonStartup } from "./background-bandwidth.js";
|
|
21
22
|
// One definition, shared with the worker's per-job check in src/disk.ts. Two
|
|
@@ -29,6 +30,7 @@ import { recoverBackgroundBandwidthLockAtDaemonStartup } from "./background-band
|
|
|
29
30
|
* exposed directly so behaviour can be tested without waiting on timers.
|
|
30
31
|
*/
|
|
31
32
|
export async function startDaemon(dataDir, options) {
|
|
33
|
+
await sweepStaleScanSnapshots(dataDir);
|
|
32
34
|
// The service manager is not a lock: an in-place upgrade or manager crash
|
|
33
35
|
// can orphan its child, then launch a replacement. Elect one complete vault
|
|
34
36
|
// worker before any heartbeat, watcher, queue, index, upload, retention or
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* between releases; these codes are a contract for the CLI, local API, and MCP.
|
|
4
4
|
* Details are operational only: never place plaintext, phrases, or keys here.
|
|
5
5
|
*/
|
|
6
|
-
export type SealkeepErrorCode = "invalid_argument" | "vault_not_initialized" | "vault_already_initialized" | "config_unsupported_version" | "recovery_phrase_missing" | "recovery_phrase_mismatch" | "recipient_key_mismatch" | "payment_required" | "archive_not_found" | "source_unreadable" | "destination_exists" | "destination_unwritable" | "insufficient_disk_space" | "scratch_space_busy" | "path_not_permitted" | "native_restore_unavailable" | "ciphertext_integrity_failed" | "plaintext_integrity_failed" | "hook_payload_invalid" | "queue_job_not_found" | "queue_lease_invalid" | "queue_lease_expired" | "queue_job_exhausted" | "daemon_already_running" | "storage_not_configured" | "storage_read_timeout" | "operation_timeout" | "cloud_account_unavailable" | "provider_unsupported" | "signer_not_configured" | "lease_expired" | "upload_incomplete" | "team_state_changed" | "team_membership_revoked" | "rate_limited" | "unauthorized" | "forbidden" | "internal";
|
|
6
|
+
export type SealkeepErrorCode = "invalid_argument" | "vault_not_initialized" | "vault_already_initialized" | "config_unsupported_version" | "recovery_phrase_missing" | "recovery_phrase_mismatch" | "recipient_key_mismatch" | "payment_required" | "archive_not_found" | "source_unreadable" | "secret_scan_blocked" | "secret_scan_failed" | "destination_exists" | "destination_unwritable" | "insufficient_disk_space" | "scratch_space_busy" | "path_not_permitted" | "native_restore_unavailable" | "ciphertext_integrity_failed" | "plaintext_integrity_failed" | "hook_payload_invalid" | "queue_job_not_found" | "queue_lease_invalid" | "queue_lease_expired" | "queue_job_exhausted" | "daemon_already_running" | "storage_not_configured" | "storage_read_timeout" | "operation_timeout" | "cloud_account_unavailable" | "provider_unsupported" | "signer_not_configured" | "lease_expired" | "upload_incomplete" | "team_state_changed" | "team_membership_revoked" | "rate_limited" | "unauthorized" | "forbidden" | "internal";
|
|
7
7
|
export declare class SealkeepError extends Error {
|
|
8
8
|
readonly code: SealkeepErrorCode;
|
|
9
9
|
readonly details: Record<string, unknown>;
|
package/dist/src/leakscan.d.ts
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
+
import { type LocalSettings } from "./machine-settings.js";
|
|
2
|
+
import type { ArchiveSecretScan } from "./types.js";
|
|
3
|
+
export type CustomScanPattern = {
|
|
4
|
+
kind: string;
|
|
5
|
+
severity: "high" | "medium";
|
|
6
|
+
source: string;
|
|
7
|
+
};
|
|
8
|
+
/** Aggregate sent to the control plane. No lines, paths, or matched text. */
|
|
9
|
+
export type OrgSecretRollup = {
|
|
10
|
+
version: 1;
|
|
11
|
+
archiveId: string;
|
|
12
|
+
scannedAt: string;
|
|
13
|
+
counts: Array<{
|
|
14
|
+
kind: string;
|
|
15
|
+
severity: "high" | "medium";
|
|
16
|
+
count: number;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
/** Reject patterns that match everything, fail to compile, or can backtrack without bound. */
|
|
20
|
+
export declare function validateCustomPattern(input: CustomScanPattern): CustomScanPattern;
|
|
21
|
+
type CustomHit = {
|
|
22
|
+
kind: string;
|
|
23
|
+
severity: "high" | "medium";
|
|
24
|
+
start: number;
|
|
25
|
+
end: number;
|
|
26
|
+
};
|
|
27
|
+
/** Runs custom detectors off the seal thread and stops them if one line runs long. */
|
|
28
|
+
export declare function matchCustomLine(line: string, patterns: readonly CustomScanPattern[], timeoutMs?: number): Promise<CustomHit[]>;
|
|
29
|
+
export declare function findingsMetadata(findings: readonly Finding[], policy: "record" | "block-high"): ArchiveSecretScan;
|
|
30
|
+
export declare function orgRollupBody(archiveId: string, scan: ArchiveSecretScan): OrgSecretRollup;
|
|
31
|
+
/**
|
|
32
|
+
* The scan result is only valid for the bytes whose SHA-256 it names.
|
|
33
|
+
* `snapshotPath`, when present, is an immutable copy of those bytes. Streamed
|
|
34
|
+
* seals read it instead of the live transcript, so a later edit cannot be
|
|
35
|
+
* uploaded. The caller deletes it when the seal finishes.
|
|
36
|
+
*/
|
|
37
|
+
export type SealScan = {
|
|
38
|
+
metadata: ArchiveSecretScan;
|
|
39
|
+
sha256: string;
|
|
40
|
+
snapshotPath?: string;
|
|
41
|
+
};
|
|
42
|
+
/** Removes a scanned snapshot. Safe to call when the scan did not take one. */
|
|
43
|
+
export declare function discardScanSnapshot(scan: SealScan | undefined): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* A crash after the snapshot is written leaves plaintext in scan-snapshots.
|
|
46
|
+
* The name records the creating pid. Startup and the next scanned seal remove
|
|
47
|
+
* a file only when that process is gone, so a live seal keeps its copy.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sweepStaleScanSnapshots(dataDir: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Set by tests to change the file after the scan has accepted it and before
|
|
52
|
+
* the seal reads it again. Production leaves this unset.
|
|
53
|
+
*/
|
|
54
|
+
export declare const sealScanTestHooks: {
|
|
55
|
+
afterAccepted?: (path: string) => Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Scan before encryption or upload. Returns metadata and the digest of the
|
|
59
|
+
* exact byte range the seal will read, or undefined when the policy is off
|
|
60
|
+
* or a fail-open scanner error skipped it. Throws before any archive exists
|
|
61
|
+
* when the seal must not proceed.
|
|
62
|
+
*/
|
|
63
|
+
export declare function scanBeforeSeal(dataDir: string, path: string, signal?: AbortSignal, hooks?: {
|
|
64
|
+
afterRead?: () => Promise<void>;
|
|
65
|
+
}, byteLength?: number, options?: {
|
|
66
|
+
snapshot?: boolean;
|
|
67
|
+
}): Promise<SealScan | undefined>;
|
|
68
|
+
/** Refuse to publish a seal whose plaintext digest is not the digest that was scanned. */
|
|
69
|
+
export declare function assertScanCoversSeal(scan: SealScan | undefined, sealedSha256: string, sourcePath: string): void;
|
|
70
|
+
/** @deprecated Use scanBeforeSeal. Kept so older call sites keep blocking. */
|
|
71
|
+
export declare function assertSecretScanPasses(dataDir: string, path: string, signal?: AbortSignal): Promise<void>;
|
|
72
|
+
export type ControlPlaneDevice = {
|
|
73
|
+
deviceId: string;
|
|
74
|
+
privateKeyPem: string;
|
|
75
|
+
};
|
|
76
|
+
export declare function readControlPlaneDevice(dataDir: string): Promise<ControlPlaneDevice | null>;
|
|
77
|
+
export declare function writeControlPlaneDevice(dataDir: string, device: ControlPlaneDevice): Promise<void>;
|
|
78
|
+
/** Best-effort delivery after a durable seal. A network failure never rolls the archive back. */
|
|
79
|
+
export declare function deliverOrgRollup(dataDir: string, archiveId: string, scan: ArchiveSecretScan | undefined): Promise<void>;
|
|
80
|
+
export type SharedScanPolicy = {
|
|
81
|
+
version: 1;
|
|
82
|
+
secretScanPolicy: LocalSettings["secretScanPolicy"];
|
|
83
|
+
secretScanFailClosed: boolean;
|
|
84
|
+
secretScanPatterns: CustomScanPattern[];
|
|
85
|
+
};
|
|
86
|
+
/** What may move between machines. Consent and the control-plane URL stay local. */
|
|
87
|
+
export declare function sharedScanPolicy(settings: LocalSettings): SharedScanPolicy;
|
|
1
88
|
export type Finding = {
|
|
2
89
|
kind: string;
|
|
3
90
|
/** 1-based, matching what editors and `sed -n` call a line. */
|
|
@@ -28,21 +115,26 @@ export type ScanReport = {
|
|
|
28
115
|
export declare function maskSecret(secret: string): string;
|
|
29
116
|
/** Character-frequency Shannon entropy, in bits per character. */
|
|
30
117
|
export declare function shannonEntropy(value: string): number;
|
|
118
|
+
type Detector = {
|
|
119
|
+
kind: string;
|
|
120
|
+
/** `dg` flags, so matchAll yields index pairs for the secret group. */
|
|
121
|
+
pattern: RegExp;
|
|
122
|
+
/** Capture group holding the secret itself; whole match when omitted. */
|
|
123
|
+
group?: number;
|
|
124
|
+
/** Post-regex gate for shapes a regex alone over-matches. */
|
|
125
|
+
validate?: (candidate: string) => boolean;
|
|
126
|
+
severity?: "high" | "medium";
|
|
127
|
+
};
|
|
31
128
|
/** Tests derive their vectors from these rather than restating the numbers. */
|
|
32
129
|
export declare const GENERIC_MIN_LENGTH = 24;
|
|
33
130
|
export declare const GENERIC_ENTROPY_THRESHOLD = 4.2;
|
|
34
|
-
export declare function scanText(text: string): Finding[];
|
|
35
|
-
|
|
36
|
-
* Line-by-line over a read stream: memory is bounded by the longest single
|
|
37
|
-
* line, never by the file, so a multi-GB jsonl scans in the same footprint as
|
|
38
|
-
* a small one. An unreadable input rejects — a scanner that swallows a read
|
|
39
|
-
* error would be reporting "clean" about bytes it never saw.
|
|
40
|
-
*/
|
|
41
|
-
export declare function scanFileStreaming(path: string, onFinding?: (finding: Finding) => void): Promise<Finding[]>;
|
|
131
|
+
export declare function scanText(text: string, patterns?: readonly CustomScanPattern[]): Finding[];
|
|
132
|
+
export declare function scanFileStreaming(path: string, onFinding?: (finding: Finding) => void, extra?: readonly Detector[]): Promise<Finding[]>;
|
|
42
133
|
/**
|
|
43
134
|
* A file is scanned as pointed at, whatever its name; a directory means every
|
|
44
135
|
* *.jsonl beneath it. Only high findings set the exit code — failing a script
|
|
45
136
|
* over a "medium" suspicion is exactly the cry-wolf behaviour the severity
|
|
46
137
|
* split exists to avoid.
|
|
47
138
|
*/
|
|
48
|
-
export declare function scanPath(target: string, onFinding?: (finding: FileFinding) => void): Promise<ScanReport>;
|
|
139
|
+
export declare function scanPath(target: string, onFinding?: (finding: FileFinding) => void, patterns?: readonly CustomScanPattern[]): Promise<ScanReport>;
|
|
140
|
+
export {};
|