dpdp-erasure-cli 1.0.0

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.
Files changed (155) hide show
  1. package/.env.example +55 -0
  2. package/Dockerfile +33 -0
  3. package/compliance.worker.yaml +64 -0
  4. package/package.json +41 -0
  5. package/src/constants/index.ts +1 -0
  6. package/src/errors/fail.ts +110 -0
  7. package/src/errors/index.ts +4 -0
  8. package/src/errors/inferer.ts +166 -0
  9. package/src/errors/registry.ts +122 -0
  10. package/src/errors/types.ts +65 -0
  11. package/src/errors/worker.ts +161 -0
  12. package/src/index.ts +328 -0
  13. package/src/lib/crypto/digest.ts +22 -0
  14. package/src/lib/crypto/encoding.ts +78 -0
  15. package/src/lib/crypto/index.ts +2 -0
  16. package/src/lib/index.ts +1 -0
  17. package/src/modules/bootstrap/index.ts +2 -0
  18. package/src/modules/bootstrap/integrity.ts +38 -0
  19. package/src/modules/bootstrap/preflight.ts +296 -0
  20. package/src/modules/cli/check-integrity.ts +48 -0
  21. package/src/modules/cli/dry-run.ts +90 -0
  22. package/src/modules/cli/graph.ts +87 -0
  23. package/src/modules/cli/index.ts +184 -0
  24. package/src/modules/cli/init.ts +115 -0
  25. package/src/modules/cli/inspect.ts +86 -0
  26. package/src/modules/cli/introspector.ts +117 -0
  27. package/src/modules/cli/keygen.ts +38 -0
  28. package/src/modules/cli/scan.ts +126 -0
  29. package/src/modules/cli/sign.ts +50 -0
  30. package/src/modules/cli/ui.ts +61 -0
  31. package/src/modules/cli/verify-schema.ts +31 -0
  32. package/src/modules/cli/verify.ts +85 -0
  33. package/src/modules/config/compatibility.ts +271 -0
  34. package/src/modules/config/index.ts +4 -0
  35. package/src/modules/config/reader.ts +149 -0
  36. package/src/modules/config/signature.ts +69 -0
  37. package/src/modules/config/validation.ts +658 -0
  38. package/src/modules/crypto/aes.ts +158 -0
  39. package/src/modules/crypto/envelope.ts +48 -0
  40. package/src/modules/crypto/hmac.ts +60 -0
  41. package/src/modules/crypto/index.ts +3 -0
  42. package/src/modules/db/drift.ts +36 -0
  43. package/src/modules/db/graph.ts +203 -0
  44. package/src/modules/db/index.ts +4 -0
  45. package/src/modules/db/migrations.ts +254 -0
  46. package/src/modules/db/sql-debug.ts +61 -0
  47. package/src/modules/engine/blob/index.ts +3 -0
  48. package/src/modules/engine/blob/s3.ts +455 -0
  49. package/src/modules/engine/blob/store.ts +236 -0
  50. package/src/modules/engine/blob/types.ts +44 -0
  51. package/src/modules/engine/helpers/identity.ts +47 -0
  52. package/src/modules/engine/helpers/index.ts +4 -0
  53. package/src/modules/engine/helpers/outbox.ts +118 -0
  54. package/src/modules/engine/helpers/runtime.ts +115 -0
  55. package/src/modules/engine/helpers/types.ts +61 -0
  56. package/src/modules/engine/index.ts +6 -0
  57. package/src/modules/engine/notifier/config.ts +147 -0
  58. package/src/modules/engine/notifier/dispatcher.ts +300 -0
  59. package/src/modules/engine/notifier/index.ts +3 -0
  60. package/src/modules/engine/notifier/payload.ts +51 -0
  61. package/src/modules/engine/notifier/reservation.ts +153 -0
  62. package/src/modules/engine/notifier/types.ts +38 -0
  63. package/src/modules/engine/shredder.ts +254 -0
  64. package/src/modules/engine/types.ts +146 -0
  65. package/src/modules/engine/vault/compiled-targets.ts +562 -0
  66. package/src/modules/engine/vault/context.ts +254 -0
  67. package/src/modules/engine/vault/dry-run.ts +94 -0
  68. package/src/modules/engine/vault/execution.ts +485 -0
  69. package/src/modules/engine/vault/index.ts +3 -0
  70. package/src/modules/engine/vault/purge.ts +82 -0
  71. package/src/modules/engine/vault/retention.ts +124 -0
  72. package/src/modules/engine/vault/satellite-mutation.ts +193 -0
  73. package/src/modules/engine/vault/satellite.ts +103 -0
  74. package/src/modules/engine/vault/shadow.ts +36 -0
  75. package/src/modules/engine/vault/static-plan.ts +116 -0
  76. package/src/modules/engine/vault/store.ts +34 -0
  77. package/src/modules/engine/vault/vault.ts +84 -0
  78. package/src/modules/introspector/classifier.ts +502 -0
  79. package/src/modules/introspector/dag.ts +276 -0
  80. package/src/modules/introspector/index.ts +7 -0
  81. package/src/modules/introspector/naming.ts +75 -0
  82. package/src/modules/introspector/report.ts +153 -0
  83. package/src/modules/introspector/run.ts +123 -0
  84. package/src/modules/introspector/s3-sampler.ts +227 -0
  85. package/src/modules/introspector/types.ts +131 -0
  86. package/src/modules/introspector/yaml.ts +101 -0
  87. package/src/modules/network/api/control-plane.ts +275 -0
  88. package/src/modules/network/api/index.ts +1 -0
  89. package/src/modules/network/api/validation.ts +71 -0
  90. package/src/modules/network/index.ts +4 -0
  91. package/src/modules/network/object-store/aws/client.ts +444 -0
  92. package/src/modules/network/object-store/aws/credentials.ts +271 -0
  93. package/src/modules/network/object-store/aws/index.ts +2 -0
  94. package/src/modules/network/object-store/aws/sigv4.ts +190 -0
  95. package/src/modules/network/object-store/aws/type.ts +6 -0
  96. package/src/modules/network/object-store/index.ts +1 -0
  97. package/src/modules/network/outbox/dispatcher.ts +183 -0
  98. package/src/modules/network/outbox/index.ts +3 -0
  99. package/src/modules/network/outbox/process.ts +133 -0
  100. package/src/modules/network/outbox/shared.ts +56 -0
  101. package/src/modules/network/outbox/store.ts +346 -0
  102. package/src/modules/network/outbox/types.ts +54 -0
  103. package/src/modules/network/request-signing.ts +61 -0
  104. package/src/modules/worker/index.ts +2 -0
  105. package/src/modules/worker/tasks.ts +58 -0
  106. package/src/modules/worker/types.ts +89 -0
  107. package/src/modules/worker/worker.ts +243 -0
  108. package/src/secrets/index.ts +4 -0
  109. package/src/secrets/kms/index.ts +2 -0
  110. package/src/secrets/kms/signature.ts +82 -0
  111. package/src/secrets/kms/validation.ts +64 -0
  112. package/src/secrets/reader.ts +42 -0
  113. package/src/secrets/repository/crypto.ts +89 -0
  114. package/src/secrets/repository/index.ts +2 -0
  115. package/src/secrets/repository/methods.ts +37 -0
  116. package/src/secrets/resolvers.ts +247 -0
  117. package/src/secrets/signature.ts +78 -0
  118. package/src/types/index.ts +1 -0
  119. package/src/types/types.ts +23 -0
  120. package/src/utils/identifiers.ts +48 -0
  121. package/src/utils/index.ts +3 -0
  122. package/src/utils/json.ts +35 -0
  123. package/src/utils/logger.ts +161 -0
  124. package/src/validation/zod.ts +70 -0
  125. package/tests/adversarial.test.ts +464 -0
  126. package/tests/blob-s3.test.ts +216 -0
  127. package/tests/config.test.ts +395 -0
  128. package/tests/control-plane-client.test.ts +108 -0
  129. package/tests/crypto.test.ts +106 -0
  130. package/tests/errors.test.ts +69 -0
  131. package/tests/fetch-dispatcher.test.ts +213 -0
  132. package/tests/graph.test.ts +84 -0
  133. package/tests/helpers/index.ts +101 -0
  134. package/tests/index-preflight.test.ts +168 -0
  135. package/tests/introspector-classifier.test.ts +62 -0
  136. package/tests/introspector-report.test.ts +85 -0
  137. package/tests/introspector.test.ts +394 -0
  138. package/tests/kms.test.ts +124 -0
  139. package/tests/logger.test.ts +61 -0
  140. package/tests/notifier.test.ts +303 -0
  141. package/tests/outbox.test.ts +478 -0
  142. package/tests/purge-policy.test.ts +124 -0
  143. package/tests/retention.test.ts +103 -0
  144. package/tests/s3-client.test.ts +110 -0
  145. package/tests/satellite.test.ts +119 -0
  146. package/tests/schema-compatibility.test.ts +237 -0
  147. package/tests/schema-integrity.test.ts +64 -0
  148. package/tests/shredder.test.ts +163 -0
  149. package/tests/vault.compiled-targets.test.ts +243 -0
  150. package/tests/vault.replica.test.ts +59 -0
  151. package/tests/vault.test.ts +279 -0
  152. package/tests/worker.retry.test.ts +291 -0
  153. package/tests/worker.test.ts +200 -0
  154. package/tsconfig.json +19 -0
  155. package/vitest.config.ts +13 -0
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ import pc from "picocolors";
4
+
5
+ const pkgPath = new URL("../../../package.json", import.meta.url);
6
+ const pkg = await Bun.file(pkgPath).json();
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name("worker")
12
+ .description("Compliance Engine Operator CLI - Safe & Professional Data Auditing.")
13
+ .version(pkg.version);
14
+
15
+ program
16
+ .command("init")
17
+ .description("Interactively provision a legal compliance manifest.")
18
+ .action(async () => {
19
+ const { initAction } = await import("./init")
20
+ });
21
+
22
+ program
23
+ .command("scan")
24
+ .description("Metadata-only schema scan for potential PII columns.")
25
+ .option("-u, --url <url>", "PostgreSQL Connection DSN")
26
+ .option("-s, --schema <schema>", "Database schema to target")
27
+ .option("--threshold <score>", "Metadata confidence threshold", "0.6")
28
+ .option("--json <path>", "Write machine-readable scan findings")
29
+ .action(async (options) => {
30
+ const { scanAction } = await import("./scan");
31
+ await scanAction(options);
32
+ });
33
+
34
+ program
35
+ .command("introspect")
36
+ .description("Offline compile FK DAG and draft PII mappings without mutating data.")
37
+ .option("-u, --url <url>", "PostgreSQL Connection DSN")
38
+ .option("-r, --root <table>", "Root table, e.g. public.users")
39
+ .option("-s, --schema <schema>", "Default database schema")
40
+ .option("-o, --output <path>", "Draft YAML output path", "compliance.worker.yml.draft")
41
+ .option("-d, --max-depth <depth>", "Recursive FK depth breaker", "32")
42
+ .option("--sample-percent <percent>", "Postgres TABLESAMPLE SYSTEM percentage", "1")
43
+ .option("--sample-limit <rows>", "Maximum sampled rows per column", "100")
44
+ .option("--threshold <score>", "PII confidence threshold", "0.75")
45
+ .option("--report <path>", "Markdown review report path")
46
+ .option("--json-report <path>", "Machine-readable JSON review report path")
47
+ .option("--fail-on-review", "Exit non-zero when findings need human review")
48
+ .option("-c, --config <path>", "Manifest path for --verify-only", "compliance.worker.yml")
49
+ .option("--verify-only", "Verify schema hash instead of generating a draft")
50
+ .action(async (options) => {
51
+ const { introspectAction } = await import("./introspector");
52
+ await introspectAction(options);
53
+ });
54
+
55
+ program
56
+ .command("verify-schema")
57
+ .description("CI/CD gate: fail when live schema differs from legal attestation hash.")
58
+ .option("-c, --config <path>", "Manifest path", "compliance.worker.yml")
59
+ .option("-u, --url <url>", "PostgreSQL Connection DSN")
60
+ .action(async (options) => {
61
+ const { verifySchemaAction } = await import("./verify-schema");
62
+ await verifySchemaAction(options);
63
+ });
64
+
65
+ program
66
+ .command("graph")
67
+ .description("Visualize recursive table dependencies for a specific root.")
68
+ .requiredOption("-t, --table <table>", "Root table name")
69
+ .option("-u, --url <url>", "PostgreSQL Connection DSN")
70
+ .option("-s, --schema <schema>", "Database schema to target")
71
+ .option("-d, --max-depth <depth>", "Safety recursion limit", "32")
72
+ .action(async (options) => {
73
+ const { graphAction } = await import("./graph");
74
+ await graphAction(options);
75
+ });
76
+
77
+ program
78
+ .command("inspect")
79
+ .description("Inspect a worker manifest and summarize legal/configuration coverage.")
80
+ .option("-c, --config <path>", "Manifest path", "compliance.worker.yml")
81
+ .action(async (options) => {
82
+ const { inspectAction } = await import("./inspect");
83
+ await inspectAction(options);
84
+ });
85
+
86
+ program
87
+ .command("check-integrity")
88
+ .description("Fail closed unless schema hash and compiled DAG match the live database.")
89
+ .option("-c, --config <path>", "Manifest path", "compliance.worker.yml")
90
+ .option("-u, --url <url>", "PostgreSQL Connection DSN")
91
+ .action(async (options) => {
92
+ const { checkIntegrityAction } = await import("./check-integrity");
93
+ await checkIntegrityAction(options);
94
+ });
95
+
96
+ program
97
+ .command("verify")
98
+ .description("Perform integrity checks and compute mandatory schema hashes.")
99
+ .option("-c, --config <path>", "Manifest path", "compliance.worker.yml")
100
+ .option("-u, --url <url>", "PostgreSQL Connection DSN")
101
+ .action(async (options) => {
102
+ const { verifyAction } = await import("./verify");
103
+ await verifyAction(options);
104
+ });
105
+
106
+ program
107
+ .command("dry-run")
108
+ .description("Simulate a vault operation without mutating production data.")
109
+ .requiredOption("-i, --id <id>", "Root subject ID (e.g. 1042)")
110
+ .option("-c, --config <path>", "Manifest path", "compliance.worker.yml")
111
+ .option("-u, --url <url>", "PostgreSQL Connection DSN")
112
+ .action(async (options) => {
113
+ const { dryRunAction } = await import("./dry-run");
114
+ await dryRunAction(options);
115
+ });
116
+
117
+ program
118
+ .command("keygen")
119
+ .description("Provision Ed25519 keys for configuration signing.")
120
+ .action(async () => {
121
+ const { keygenAction } = await import("./keygen");
122
+ await keygenAction();
123
+ });
124
+
125
+ program
126
+ .command("sign")
127
+ .description("Apply a cryptographically secure signature to the manifest.")
128
+ .option("-c, --config <path>", "Manifest path", "compliance.worker.yml")
129
+ .option("-k, --key <path>", "Private key file path")
130
+ .action(async (options) => {
131
+ const { signAction } = await import("./sign");
132
+ await signAction(options);
133
+ });
134
+
135
+ program.on("command:*", () => {
136
+ console.error(pc.red(`\nInvalid command: ${pc.bold(program.args.join(" "))}`));
137
+ console.error(`Refer to ${pc.cyan("compliance-worker --help")} for available utilities.\n`);
138
+ process.exit(1);
139
+ });
140
+
141
+ const rawArgs = process.argv.slice(2);
142
+
143
+ if (rawArgs.length === 0) {
144
+ if (process.stdout.isTTY) {
145
+ try {
146
+ const { select } = await import("@inquirer/prompts");
147
+ console.log(pc.cyan(`\n💼 Compliance Engine Operator Interactive Console [v${pkg.version}]`));
148
+
149
+ const choice = await select({
150
+ message: "Select a compliance operation utility to run:",
151
+ choices: [
152
+ { name: "Initialize legal compliance manifest (init)", value: "init" },
153
+ { name: "Metadata-only schema scan for PII (scan)", value: "scan" },
154
+ { name: "Offline compile FK DAG and draft mappings (introspect)", value: "introspect" },
155
+ { name: "CI/CD check: Verify schema integrity (verify-schema)", value: "verify-schema" },
156
+ { name: "Visualize recursive table dependencies (graph)", value: "graph" },
157
+ { name: "Simulate an erasure/mutation safely (dry-run)", value: "dry-run" },
158
+ { name: "Exit Console", value: "exit" },
159
+ ],
160
+ });
161
+
162
+ if (choice === "exit") {
163
+ process.exit(0);
164
+ }
165
+
166
+ const simulatedArgv: string[] = [
167
+ Bun.argv[0] ?? "bun",
168
+ Bun.argv[1] ?? import.meta.path,
169
+ choice
170
+ ];
171
+ program.parse(simulatedArgv);
172
+
173
+ } catch {
174
+ process.exit(0);
175
+ }
176
+ } else {
177
+ program.outputHelp();
178
+ process.exit(0);
179
+ }
180
+ } else {
181
+ program.parse(process.argv);
182
+ }
183
+
184
+ program.parse();
@@ -0,0 +1,115 @@
1
+ import { input, number } from "@inquirer/prompts";
2
+ import yaml from "js-yaml";
3
+ import { UI, exitWithError } from "./ui";
4
+
5
+ /**
6
+ * Interactive Configuration Wizard.
7
+ * Forces explicit data entry for critical legal fields to ensure liability shift.
8
+ */
9
+ export async function initAction() {
10
+ UI.header("Configuration Setup");
11
+
12
+ UI.info("This wizard will help you generate 'compliance.worker.yml'.");
13
+ UI.info("This manifest acts as the legal contract for your Data Plane Worker.");
14
+
15
+ try {
16
+ const appSchema = await input({
17
+ message: "Application Schema (e.g. 'public'):",
18
+ validate: (v) => v.trim().length > 0 || "Required.",
19
+ });
20
+
21
+ const engineSchema = await input({
22
+ message: "Compliance Engine Schema (e.g. 'compliance_engine'):",
23
+ default: "compliance_engine",
24
+ });
25
+
26
+ const rootTable = await input({
27
+ message: "Root Table for Graph Discovery (e.g. 'users'):",
28
+ validate: (v) => v.trim().length > 0 || "Required.",
29
+ });
30
+
31
+ const rootIdColumn = await input({
32
+ message: "Root Primary Key Column (e.g. 'id'):",
33
+ default: "id",
34
+ });
35
+
36
+ const dpoIdentifier = await input({
37
+ message: "DPO Identifier (Email or UID):",
38
+ validate: (v) => v.trim().length > 0 || "Required for legal audit trail.",
39
+ });
40
+
41
+ const defaultRetention = await number({
42
+ message: "Default Retention Period (years):",
43
+ validate: (v) => (v !== undefined && v >= 0) || "Must be 0 or greater.",
44
+ });
45
+
46
+ const config = {
47
+ version: "1.0",
48
+ database: {
49
+ app_schema: appSchema.trim(),
50
+ engine_schema: engineSchema.trim(),
51
+ },
52
+ compliance_policy: {
53
+ default_retention_years: defaultRetention,
54
+ notice_window_hours: 48,
55
+ retention_rules: [
56
+ {
57
+ rule_name: "PMLA_FINANCIAL_EXAMPLE",
58
+ legal_citation: "E.g. Sec 12 of PMLA 2002",
59
+ if_has_data_in: ["transactions"],
60
+ retention_years: 10
61
+ }
62
+ ],
63
+ },
64
+ graph: {
65
+ root_table: rootTable.trim(),
66
+ root_id_column: rootIdColumn.trim(),
67
+ max_depth: 32,
68
+ root_pii_columns: {
69
+ email: "HMAC",
70
+ full_name: "STATIC_MASK"
71
+ },
72
+ },
73
+ satellite_targets: [],
74
+ blob_targets: [],
75
+ outbox: {
76
+ batch_size: 10,
77
+ lease_seconds: 60,
78
+ max_attempts: 10,
79
+ base_backoff_ms: 1000,
80
+ },
81
+ security: {
82
+ notification_lease_seconds: 120,
83
+ },
84
+ integrity: {
85
+ expected_schema_hash: "0".repeat(64),
86
+ },
87
+ legal_attestation: {
88
+ dpo_identifier: dpoIdentifier.trim(),
89
+ configuration_version: "1.0.0",
90
+ legal_review_date: new Date().toISOString().split("T")[0],
91
+ acknowledgment: "I confirm this configuration accurately reflects our legal data retention obligations.",
92
+ },
93
+ };
94
+
95
+ const yamlText = yaml.dump(config, {
96
+ indent: 2,
97
+ lineWidth: -1,
98
+ quotingType: '"',
99
+ });
100
+
101
+ await Bun.write("compliance.worker.yml", yamlText);
102
+
103
+ UI.success("Manifest generated: compliance.worker.yml");
104
+
105
+ UI.info("\nRECOMMENDED NEXT STEPS:");
106
+ UI.step(1, "PII Discovery: Run 'compliance-worker scan' to identify sensitive columns.");
107
+ UI.step(2, "Integrity Check: Run 'compliance-worker verify' to compute the schema hash.");
108
+ UI.step(3, "Simulation: Run 'compliance-worker dry-run --id <id>' to preview effects.");
109
+ } catch (err) {
110
+ if (err instanceof Error && err.name === "ExitPromptError") {
111
+ process.exit(0);
112
+ }
113
+ exitWithError("Initialization failed", err instanceof Error ? err.message : String(err));
114
+ }
115
+ }
@@ -0,0 +1,86 @@
1
+ import { readWorkerConfig, type CompiledExecutionTarget, type WorkerConfig } from "@modules/config";
2
+ import pc from "picocolors";
3
+ import { exitWithError, UI } from "./ui";
4
+
5
+ function countCompiledTargets(config: WorkerConfig): number {
6
+ return (config.rules ?? []).reduce((count, rule) => count + rule.targets.length, 0);
7
+ }
8
+
9
+ function countCompiledPiiColumns(config: WorkerConfig): number {
10
+ return (config.rules ?? []).reduce(
11
+ (count, rule) => count + rule.targets.reduce((targetCount, target) => targetCount + target.pii_columns.length, 0),
12
+ 0
13
+ );
14
+ }
15
+
16
+ function renderTarget(target: CompiledExecutionTarget): string {
17
+ const pii = target.pii_columns.length > 0 ? target.pii_columns.join(", ") : "none";
18
+ const action = target.action ?? "none";
19
+ return `${target.table} | action=${action} | pii=${pii}`;
20
+ }
21
+
22
+ /**
23
+ * Inspects a worker manifest without connecting to the database.
24
+ *
25
+ * The command is intentionally read-only. It validates YAML shape, resolves local key material using
26
+ * placeholder keys when needed, and prints the legal/configuration coverage developers must review
27
+ * before running `check-integrity` against a live database.
28
+ *
29
+ * @param options - CLI options containing the manifest path.
30
+ * @returns Resolves after printing the manifest summary.
31
+ */
32
+ export async function inspectAction(options: { config: string }): Promise<void> {
33
+ UI.header("Manifest Inspection");
34
+
35
+ const mockEnv = {
36
+ ...process.env,
37
+ DPDP_MASTER_KEY: process.env.DPDP_MASTER_KEY || "0".repeat(64),
38
+ DPDP_HMAC_KEY: process.env.DPDP_HMAC_KEY || "0".repeat(64),
39
+ };
40
+
41
+ let config: WorkerConfig;
42
+ try {
43
+ config = await readWorkerConfig(mockEnv, options.config);
44
+ } catch (error) {
45
+ exitWithError("Manifest inspection failed", error instanceof Error ? error.message : String(error));
46
+ }
47
+
48
+ UI.step(1, "Legal Attestation");
49
+ UI.keyValue("DPO", config.legal_attestation.dpo_identifier);
50
+ UI.keyValue("Version", config.legal_attestation.configuration_version);
51
+ UI.keyValue("Review Date", config.legal_attestation.legal_review_date);
52
+ UI.keyValue("Schema Hash", config.legal_attestation.schema_hash ?? config.integrity.expected_schema_hash);
53
+ UI.keyValue("Generated By", config.legal_attestation.generated_by ?? "manual");
54
+
55
+ UI.step(2, "Database Boundary");
56
+ UI.keyValue("App Schema", config.database.app_schema);
57
+ UI.keyValue("Engine Schema", config.database.engine_schema);
58
+ UI.keyValue("Root Table", config.graph.root_table);
59
+ UI.keyValue("Root ID Column", config.graph.root_id_column);
60
+ UI.keyValue("Root PII Columns", Object.keys(config.graph.root_pii_columns).join(", "));
61
+
62
+ UI.step(3, "Execution Coverage");
63
+ UI.keyValue("Compiled Rules", String((config.rules ?? []).length));
64
+ UI.keyValue("Compiled Targets", String(countCompiledTargets(config)));
65
+ UI.keyValue("Compiled PII Columns", String(countCompiledPiiColumns(config)));
66
+ UI.keyValue("Satellite Targets", String(config.satellite_targets.length));
67
+ UI.keyValue("Blob Targets", String(config.blob_targets.length));
68
+ UI.keyValue("Retention Rules", String(config.compliance_policy.retention_rules.length));
69
+
70
+ if ((config.rules ?? []).length === 0) {
71
+ UI.warn("No compiled DAG rules found. Run compliance-worker introspect and review the generated targets.");
72
+ } else {
73
+ const table = UI.table(["Rule", "Target"]);
74
+ for (const rule of config.rules ?? []) {
75
+ for (const target of rule.targets) {
76
+ table.push([pc.bold(rule.id), renderTarget(target)]);
77
+ }
78
+ }
79
+ console.log(table.toString());
80
+ }
81
+
82
+ UI.step(4, "Next Checks");
83
+ UI.subStep("Run compliance-worker check-integrity --config <path> against the target database.");
84
+ UI.subStep("Run compliance-worker sign --config <path> after DPO approval.");
85
+ UI.subStep("Deploy only signed manifests with matching schema hashes.");
86
+ }
@@ -0,0 +1,117 @@
1
+ import postgres from "postgres";
2
+ import pc from "picocolors";
3
+ import type { IntrospectorCliOptions } from "../introspector/types";
4
+ import { exitWithError, UI } from "./ui";
5
+ import { runIntrospector, verifySchemaIntegrity } from "../introspector";
6
+ import { readWorkerConfig } from "../config";
7
+ import { buildIntrospectorReport, renderIntrospectorJson, renderIntrospectorMarkdown } from "../introspector/report";
8
+
9
+ function parseNumber(value: string | undefined, fallback: number): number {
10
+ if (value === undefined) {
11
+ return fallback;
12
+ }
13
+
14
+ const parsed = Number(value);
15
+ if (!Number.isFinite(parsed)) {
16
+ exitWithError("Invalid numeric option.", `${value} is not a finite number.`);
17
+ }
18
+ return parsed;
19
+ }
20
+
21
+ /**
22
+ * Executes the offline Introspector CLI and writes `compliance.worker.yml.draft`.
23
+ *
24
+ * @param options - CLI flags provided by Commander.
25
+ */
26
+ export async function introspectAction(options: IntrospectorCliOptions): Promise<void> {
27
+ UI.header("Offline Introspector");
28
+
29
+ const dbUrl = options.url ?? process.env.DATABASE_URL;
30
+ if (!dbUrl) {
31
+ exitWithError("Database URL required.", "Pass --url or set DATABASE_URL env");
32
+ }
33
+
34
+ if (options.verifyOnly) {
35
+ const sql = postgres(dbUrl);
36
+ const configPath = options.config ?? "compliance.worker.yml";
37
+ const spinner = UI.spinner(`Verifying schema hash against ${configPath}`);
38
+ try {
39
+ const liveHash = await verifySchemaIntegrity({ sql, configPath });
40
+ spinner.succeed("Schema hash matches legal attestation");
41
+ UI.keyValue("Live Schema Hash", liveHash);
42
+ return;
43
+ } catch (err) {
44
+ spinner.fail("Schema verification failed");
45
+ exitWithError("Privacy-as-Code gate failed", err instanceof Error ? err.message : String(err));
46
+ } finally {
47
+ await sql.end();
48
+ }
49
+ }
50
+
51
+ let rootTable = options.root;
52
+ let appSchema = options.schema;
53
+ if (!rootTable || !appSchema) {
54
+ try {
55
+ const config = await readWorkerConfig(process.env);
56
+ rootTable = rootTable ?? `${config.database.app_schema}.${config.graph.root_table}`;
57
+ appSchema = appSchema ?? config.database.app_schema;
58
+ UI.info(`Detected root table from manifest: ${pc.bold(rootTable)}`);
59
+ } catch (err) {
60
+ if (!rootTable) {
61
+ exitWithError("Root table required.", "Pass --root public.users or provide compliance.worker.yml");
62
+ }
63
+ appSchema = appSchema ?? "public";
64
+ }
65
+ }
66
+
67
+ const output = options.output ?? "compliance.worker.yml.draft";
68
+ const sql = postgres(dbUrl, { max: 1 });
69
+ const spinner = UI.spinner(`Compiling static DAG and bounded PII taxomony for ${rootTable}...`);
70
+
71
+ try {
72
+ const { draft, yaml } = await runIntrospector({
73
+ sql,
74
+ rootTable,
75
+ defaultSchema: appSchema,
76
+ maxDepth: parseNumber(options.maxDepth, 32),
77
+ samplePercent: parseNumber(options.samplePercent, 1),
78
+ sampleLimit: parseNumber(options.sampleLimit, 100),
79
+ threshold: parseNumber(options.threshold, 0.75),
80
+ });
81
+
82
+ await Bun.write(output, yaml);
83
+ const report = await buildIntrospectorReport(draft);
84
+ const markdownReportPath = options.report ?? `${output}.report.md`;
85
+ await Bun.write(markdownReportPath, renderIntrospectorMarkdown(report));
86
+ if (options.jsonReport) {
87
+ await Bun.write(options.jsonReport, renderIntrospectorJson(report));
88
+ }
89
+
90
+ spinner.succeed("Introspector draft generated");
91
+ UI.keyValue("Output", output);
92
+ UI.keyValue("Report", markdownReportPath);
93
+ if (options.jsonReport) {
94
+ UI.keyValue("JSON Report", options.jsonReport);
95
+ }
96
+ UI.keyValue("Root", `${draft.root.schema}.${draft.root.table}`);
97
+ UI.keyValue("Targets", String(draft.targets.length));
98
+ UI.keyValue("PII Columns", String(report.summary.piiColumnCount));
99
+ UI.keyValue("Review Findings", String(report.summary.reviewRequiredCount));
100
+ UI.keyValue("Logical Links", String(report.summary.potentialLogicalLinkCount));
101
+ UI.warn("Draft is not a production manifest. DPO review and legal attestation are mandatory before use.");
102
+ if (
103
+ options.failOnReview &&
104
+ (report.summary.reviewRequiredCount > 0 || report.summary.potentialLogicalLinkCount > 0)
105
+ ) {
106
+ exitWithError(
107
+ "Manual review required.",
108
+ "The draft contains lower-confidence PII findings or potential logical links. Review the report before CI promotion."
109
+ );
110
+ }
111
+ } catch (err) {
112
+ spinner.fail("Introspector failed");
113
+ exitWithError("Offline introspection error", err instanceof Error ? err.message : String(err));
114
+ } finally {
115
+ await sql.end();
116
+ }
117
+ }
@@ -0,0 +1,38 @@
1
+ import pc from "picocolors";
2
+ import { bytesToBase64 } from "@/lib";
3
+ import { UI, exitWithError } from "./ui";
4
+
5
+ /**
6
+ * Generates an Ed25519 keypair for manifest signing.
7
+ */
8
+ export async function keygenAction() {
9
+ UI.header("Key Generation");
10
+ UI.info("Generating cryptographically secure Ed25519 keypair...");
11
+
12
+ try {
13
+ const keyPair = (await globalThis.crypto.subtle.generateKey(
14
+ { name: "Ed25519" },
15
+ true,
16
+ ["sign", "verify"],
17
+ )) as unknown as CryptoKeyPair;
18
+
19
+ const privateKeyRaw = await globalThis.crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
20
+ const publicKeyRaw = await globalThis.crypto.subtle.exportKey("pkcs8", keyPair.publicKey);
21
+
22
+ const privateKeyBase64 = bytesToBase64(new Uint8Array(privateKeyRaw));
23
+ const publicKeyBase64 = bytesToBase64(new Uint8Array(publicKeyRaw));
24
+
25
+ await Bun.write("worker.pkcs8.key", privateKeyBase64);
26
+ await Bun.write("worker.spki.pub", publicKeyBase64);
27
+
28
+ UI.success("Keypair provisioned successfully.");
29
+ UI.info("FILES GENERATED:");
30
+ UI.keyValue("Private Key", pc.red("worker.pkcs8.key") + " (SECRET)");
31
+ UI.keyValue("Public Key ", pc.green("worker.spki.pub") + " (AUDIT)");
32
+
33
+ UI.warn("Never share your private key or commit it to version control.");
34
+ UI.hint("Configuration signing provides tamper-evidence for your legal mandates.");
35
+ } catch (err) {
36
+ exitWithError("Key generation failed", err instanceof Error ? err.message : String(err));
37
+ }
38
+ }
@@ -0,0 +1,126 @@
1
+ import postgres from "postgres";
2
+ import pc from "picocolors";
3
+ import { readWorkerConfig } from "@modules/config";
4
+ import { metadataScore } from "@modules/introspector";
5
+ import { UI, exitWithError } from "./ui";
6
+
7
+ interface ScanOptions {
8
+ url?: string;
9
+ schema?: string;
10
+ threshold?: string;
11
+ json?: string;
12
+ }
13
+
14
+ interface ScanColumnRow {
15
+ table_name: string;
16
+ column_name: string;
17
+ data_type: string;
18
+ }
19
+
20
+ interface ScanFinding {
21
+ table: string;
22
+ column: string;
23
+ dataType: string;
24
+ metadataScore: number;
25
+ }
26
+
27
+ function parseThreshold(value: string | undefined): number {
28
+ const threshold = Number(value ?? "0.6");
29
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
30
+ exitWithError("Invalid metadata threshold.", "Use a numeric value between 0 and 1.");
31
+ }
32
+
33
+ return threshold;
34
+ }
35
+
36
+ function groupFindings(findings: readonly ScanFinding[]): Map<string, ScanFinding[]> {
37
+ const grouped = new Map<string, ScanFinding[]>();
38
+ for (const finding of findings) {
39
+ const existing = grouped.get(finding.table) ?? [];
40
+ existing.push(finding);
41
+ grouped.set(finding.table, existing);
42
+ }
43
+
44
+ return grouped;
45
+ }
46
+
47
+ /**
48
+ * Scans column names with the same metadata taxonomy used by the full Introspector.
49
+ *
50
+ * @param options - Database URL, schema, threshold, and optional JSON output path.
51
+ * @returns Resolves after printing or writing metadata-only findings.
52
+ */
53
+ export async function scanAction(options: ScanOptions): Promise<void> {
54
+ UI.header("Metadata PII Scanner");
55
+
56
+ const dbUrl = options.url || process.env.DATABASE_URL;
57
+ if (!dbUrl) exitWithError("Database URL required.", "Pass --url or set DATABASE_URL env.");
58
+
59
+ let appSchema = options.schema;
60
+ if (!appSchema) {
61
+ try {
62
+ const config = await readWorkerConfig(process.env);
63
+ appSchema = config.database.app_schema;
64
+ UI.info(`Target schema detected: ${pc.bold(appSchema)}`);
65
+ } catch (error) {
66
+ exitWithError("Application schema missing.", "Provide --schema or ensure compliance.worker.yml exists");
67
+ }
68
+ }
69
+
70
+ const threshold = parseThreshold(options.threshold);
71
+ const spinner = UI.spinner(`Scanning information_schema for ${appSchema}...`);
72
+ const sql = postgres(dbUrl);
73
+
74
+ try {
75
+ const columns = await sql<ScanColumnRow[]>`
76
+ SELECT table_name, column_name, data_type
77
+ FROM information_schema.columns
78
+ WHERE table_schema = ${appSchema}
79
+ ORDER BY table_name, column_name
80
+ `;
81
+ spinner.stop();
82
+
83
+ const table = UI.table(["Table Name", "Sensitive Column Candidates"]);
84
+ const findings = columns
85
+ .map((column): ScanFinding => ({
86
+ table: column.table_name,
87
+ column: column.column_name,
88
+ dataType: column.data_type,
89
+ metadataScore: metadataScore(column.column_name)
90
+ }))
91
+ .filter((finding) => finding.metadataScore >= threshold)
92
+ .sort((left, right) => {
93
+ const byScore = right.metadataScore - left.metadataScore;
94
+ if (byScore !== 0) return byScore;
95
+ return `${left.table}.${left.column}`.localeCompare(`${right.table}.${right.column}`);
96
+ });
97
+
98
+ if (options.json) {
99
+ await Bun.write(options.json, `${JSON.stringify({ schema: appSchema, threshold, findings }, null, 2)}\n`)
100
+ UI.keyValue("JSON Output", options.json);
101
+ }
102
+
103
+ const grouped = groupFindings(findings);
104
+ if (grouped.size === 0) {
105
+ UI.success("Audit complete: Zero potention PII columns found.");
106
+ } else {
107
+ for (const [tableName, tableFindings] of grouped.entries()) {
108
+ table.push([
109
+ tableName,
110
+ tableFindings
111
+ .map((finding) => `${finding.column} (${finding.metadataScore.toFixed(2)})`)
112
+ .join(","),
113
+ ]);
114
+ }
115
+ console.log(table.toString());
116
+ UI.warn(`${grouped.size} tables contain metadata-level sensitive data candidates.`);
117
+ UI.info("Run compliance-worker introspect for bounded content sampling and YAML generation.");
118
+ }
119
+
120
+ } catch (err) {
121
+ spinner.fail("Scan failed");
122
+ exitWithError("Database audit error", err instanceof Error ? err.message : String(err));
123
+ } finally {
124
+ await sql.end();
125
+ }
126
+ }