[object Object]

← back to Secrets Manager

Add secret freshness report command

76a6bf346810a0ec379f15e9c165d9633a2244f1 · 2026-08-28 22:18:37 -0700 · Steve Abrams

Files touched

Diff

commit 76a6bf346810a0ec379f15e9c165d9633a2244f1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 22:18:37 2026 -0700

    Add secret freshness report command
---
 cli.js                      | 69 ++++++++++++++++++++++++++++++++++++++++++---
 test/verify-all.test.js     | 61 +++++++++++++++++++++++++++++++++++++++
 verification/e2e-proof.json | 18 ++++++++++++
 3 files changed, 144 insertions(+), 4 deletions(-)

diff --git a/cli.js b/cli.js
index 6d41835..59e5a24 100755
--- a/cli.js
+++ b/cli.js
@@ -6,6 +6,7 @@
 //   node cli.js add <KEY> <VALUE>
 //   node cli.js import-paste              # reads KEY=VALUE block from stdin
 //   node cli.js check <KEY>
+//   node cli.js verify-all               # verify configured keys; write data/latest.json
 //   node cli.js sync                      # re-fan master → all destinations
 //   node cli.js audit                     # scan home for leaked secrets
 //
@@ -265,6 +266,61 @@ async function verifyToken(key, value) {
   }
 }
 
+function configuredSecretKeys(routes = ROUTES) {
+  const isSecretKey = (key) => /^[A-Z][A-Z0-9_]*$/.test(key);
+  const keys = new Set(Object.keys(routes.services || {}).filter(isSecretKey));
+  for (const [key, value] of Object.entries(routes)) {
+    if (isSecretKey(key) && value && typeof value === 'object') keys.add(key);
+  }
+  return [...keys].sort();
+}
+
+async function buildVerifyAllReport({ master, routes = ROUTES, verifier = verifyToken, now = new Date() }) {
+  const checks = [];
+  const lookup = (key) => (routes.services && routes.services[key]) || routes[key] || null;
+  for (const key of configuredSecretKeys(routes)) {
+    const cfg = lookup(key);
+    const value = master[key];
+    if (!value) {
+      checks.push({ key, outcome: cfg?.verify ? 'FAIL' : 'WARN', reason: 'missing-from-master' });
+      continue;
+    }
+    if (!cfg?.verify) {
+      checks.push({ key, outcome: 'WARN', reason: 'no-verify-endpoint' });
+      continue;
+    }
+    const result = await verifier(key, value);
+    checks.push(result.ok
+      ? { key, outcome: 'PASS', http_status: result.status || null }
+      : { key, outcome: 'FAIL', http_status: result.status || null, reason: result.error ? 'network-error' : 'provider-rejected' });
+  }
+  const counts = checks.reduce((acc, check) => { acc[check.outcome]++; return acc; }, { PASS: 0, WARN: 0, FAIL: 0 });
+  return {
+    schema_version: 1,
+    generated_at: now.toISOString(),
+    status: counts.FAIL ? 'FAIL' : counts.WARN ? 'WARN' : 'PASS',
+    summary: { total: checks.length, ...counts },
+    checks,
+  };
+}
+
+async function cmdVerifyAll(options = {}) {
+  const report = await buildVerifyAllReport({
+    master: options.master || loadEnvFile(MASTER_ENV),
+    routes: options.routes || ROUTES,
+    verifier: options.verifier || verifyToken,
+    now: options.now || new Date(),
+  });
+  const outputPath = options.outputPath || path.join(ROOT, 'data', 'latest.json');
+  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
+  const tempPath = `${outputPath}.tmp-${process.pid}`;
+  fs.writeFileSync(tempPath, JSON.stringify(report, null, 2) + '\n');
+  fs.renameSync(tempPath, outputPath);
+  console.log(`verify-all ${report.status} — PASS=${report.summary.PASS} WARN=${report.summary.WARN} FAIL=${report.summary.FAIL}`);
+  console.log(`report: ${outputPath}`);
+  return report;
+}
+
 // ─── fan-out ────────────────────────────────────────────────────────────
 function fanOut(key, value) {
   const destinations = routeFor(key)?.destinations || [];
@@ -560,18 +616,23 @@ function cmdRegen(args) {
 }
 
 // ─── dispatch ───────────────────────────────────────────────────────────
-const [, , cmd, ...rest] = process.argv;
-(async () => {
+async function main(argv = process.argv.slice(2)) {
+  const [cmd, ...rest] = argv;
   switch (cmd) {
     case 'add':           await cmdAdd(rest[0], rest[1]); break;
     case 'import-paste':  await cmdImportPaste(); break;
     case 'list':          cmdList(); break;
     case 'check':         await cmdCheck(rest[0]); break;
+    case 'verify-all':    await cmdVerifyAll(); break;
     case 'sync':          cmdSync(); break;
     case 'audit':         cmdAudit(); break;
     case 'regen':         cmdRegen(rest); break;
     default:
-      console.error('usage: cli.js <add|import-paste|list|check|sync|audit|regen> [args]');
+      console.error('usage: cli.js <add|import-paste|list|check|verify-all|sync|audit|regen> [args]');
       process.exit(2);
   }
-})().catch(e => { console.error('ERR:', e.message); process.exit(1); });
+}
+
+if (require.main === module) main().catch(e => { console.error('ERR:', e.message); process.exit(1); });
+
+module.exports = { buildVerifyAllReport, cmdVerifyAll, configuredSecretKeys };
diff --git a/test/verify-all.test.js b/test/verify-all.test.js
new file mode 100644
index 0000000..41ba9f1
--- /dev/null
+++ b/test/verify-all.test.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { buildVerifyAllReport, cmdVerifyAll } = require('../cli.js');
+
+const routes = {
+  _comment: { text: 'metadata is not a secret' },
+  leak_patterns: [{ regex: 'unused' }],
+  services: {
+    GOOD_KEY: { verify: { url: 'https://unused.test' } },
+    BAD_KEY: { verify: { url: 'https://unused.test' } },
+    MANUAL_KEY: { label: 'manual only' },
+    MISSING_KEY: { verify: { url: 'https://unused.test' } },
+  },
+};
+const master = { GOOD_KEY: 'secret-good', BAD_KEY: 'secret-bad', MANUAL_KEY: 'secret-manual' };
+
+test('builds a deterministic FAIL report without retaining secret values or response bodies', async () => {
+  const seen = [];
+  const report = await buildVerifyAllReport({
+    master, routes, now: new Date('2026-08-29T05:20:00Z'),
+    verifier: async (key, value) => {
+      seen.push([key, value]);
+      return key === 'GOOD_KEY' ? { ok: true, status: 200, body: value } : { ok: false, status: 401, body: value };
+    },
+  });
+  assert.equal(report.status, 'FAIL');
+  assert.deepEqual(report.summary, { total: 4, PASS: 1, WARN: 1, FAIL: 2 });
+  assert.deepEqual(seen.map(([key]) => key), ['BAD_KEY', 'GOOD_KEY']);
+  const serialized = JSON.stringify(report);
+  assert.doesNotMatch(serialized, /secret-good|secret-bad|secret-manual/);
+  assert.doesNotMatch(serialized, /body/);
+  assert.equal(report.generated_at, '2026-08-29T05:20:00.000Z');
+});
+
+test('status is WARN for unverified keys and PASS when every configured key verifies', async () => {
+  const warn = await buildVerifyAllReport({ master: { MANUAL_KEY: 'x' }, routes: { services: { MANUAL_KEY: {} } } });
+  assert.equal(warn.status, 'WARN');
+  const pass = await buildVerifyAllReport({
+    master: { GOOD_KEY: 'x' }, routes: { services: { GOOD_KEY: { verify: {} } } },
+    verifier: async () => ({ ok: true, status: 204 }),
+  });
+  assert.equal(pass.status, 'PASS');
+});
+
+test('writes the latest report atomically for local consumers without network calls', async (t) => {
+  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'secret-freshness-'));
+  t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+  const outputPath = path.join(dir, 'data', 'latest.json');
+  const report = await cmdVerifyAll({
+    master: { GOOD_KEY: 'x' }, routes: { services: { GOOD_KEY: { verify: {} } } },
+    verifier: async () => ({ ok: true, status: 200 }), outputPath,
+    now: new Date('2026-08-29T05:21:00Z'),
+  });
+  assert.equal(report.status, 'PASS');
+  assert.deepEqual(JSON.parse(fs.readFileSync(outputPath, 'utf8')), report);
+});
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
new file mode 100644
index 0000000..726c562
--- /dev/null
+++ b/verification/e2e-proof.json
@@ -0,0 +1,18 @@
+{
+  "intent": "Produce a machine-readable PASS/WARN/FAIL freshness report for configured secrets without exposing secret values.",
+  "risk_tier": "R1 isolated CLI/report code; live provider verification and scheduling were not invoked",
+  "environment": "local Node tests with injected zero-network verifier and temporary output path",
+  "timestamp": "2026-08-29T05:21:00Z",
+  "ticket": "TK-10950-add-read-only-secret-freshness-report",
+  "build_identity": "git parent b712be4 plus owned Cycle 13 diff",
+  "checks": [
+    { "verdict": "PASS", "boundary": "classification", "command": "node --test test/verify-all.test.js", "assertions": "PASS/WARN/FAIL aggregation covers verified, rejected, missing, and no-endpoint keys" },
+    { "verdict": "PASS", "boundary": "secret redaction", "command": "node --test test/verify-all.test.js", "assertions": "report serialization contains no input secret values or provider bodies" },
+    { "verdict": "PASS", "boundary": "report artifact", "command": "node --test test/verify-all.test.js", "assertions": "temporary data/latest.json round-trips to the returned schema" },
+    { "verdict": "PASS", "boundary": "syntax/diff", "command": "node --check cli.js && git diff --check", "assertions": "CLI parses and diff is clean" }
+  ],
+  "negative_checks": ["provider rejection", "configured key missing from master", "key lacking a verify endpoint", "provider body containing the secret"],
+  "side_effects": "temporary test report only; no provider request, secret write, route/registry change, launchd install, restart, deploy, or send",
+  "cleanup": "temporary directory removed by test",
+  "verdict": "PASS for the local report pipeline"
+}

← b712be4 register Shopify full-access token routes  ·  back to Secrets Manager  ·  Harden secret freshness verification failures f585023 →