trooth 0.2.0 → 0.4.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 (3) hide show
  1. package/README.md +147 -37
  2. package/bin/trooth.mjs +446 -232
  3. package/package.json +21 -20
package/README.md CHANGED
@@ -1,12 +1,16 @@
1
1
  # trooth
2
2
 
3
- The trust network your terminal can check. Read any company's **witnessed** trust
4
- standing from the [Trooth Network](https://trooth.co). Identity, security, privacy,
5
- AI practices, and more, each witnessed, signed, and dated. It also scans your own Terraform
6
- plans against SOC 2, ISO 27001, GDPR, HIPAA, NIST AI RMF, and the EU AI Act.
3
+ The Trooth Network from your terminal.
4
+
5
+ The Trooth Network gives every company one public, signed record: what it is,
6
+ what it sells, who runs it, where its data lives, how it handles AI, and what a
7
+ buyer needs before signing. DNS tells a machine where a company is. A TLS
8
+ certificate tells it the connection is authentic. Neither says anything about
9
+ the company itself. The Trooth Network is that layer, and this CLI reads it.
7
10
 
8
11
  **`check` reads only public, already-published records. No key, no account.**
9
- **`scan` is advisory and report-only. Trooth never applies changes to your infrastructure.**
12
+ **`lint` is entirely local: it opens files and opens no sockets.**
13
+ **Trooth publishes facts and counts, never one number that sums a company up.**
10
14
 
11
15
  ## Install / run
12
16
 
@@ -19,17 +23,39 @@ npm i -g trooth
19
23
  trooth check trooth.co
20
24
  ```
21
25
 
26
+ Requires Node 18+ (uses built-in `fetch`). Zero dependencies.
27
+
22
28
  ## Commands
23
29
 
24
30
  | Command | What it does |
25
31
  | --- | --- |
26
- | `trooth check <domain>` | Reads a company's witnessed standing from the live Trooth Network (`GET https://api.trooth.co/directory/api/vendors`) and prints the score, the per-discipline breakdown, live probes, attestations, and the badge / scan IDs you can verify. Returns an honest "not listed yet" when a company has no published standing. |
27
- | `trooth scan <plan>` | Posts your `terraform show -json` plan to Trooth Pre-Flight (`POST https://api.trooth.co/v1/preflight`) and prints the verdict, score, and findings. Accepts a JSON file or a binary `.tfplan` (it runs `terraform show -json` for you). |
28
- | `trooth eu ...` | Pushes EU AI Act evidence (datasets, evals, drift) into Trooth from CI/CD. Needs a session token or company id. |
29
- | `trooth lint [path]` | Local, read-only IaC drift check. Never transmits your code. |
32
+ | `trooth check <domain>` | Reads a company's record from the live Trooth Network (`GET https://api.trooth.co/directory/api/vendors`) and prints whether it is listed, when it was witnessed and first published, the live-probe and attestation counts, the badge id, and the signature and key id you can verify. Prints an honest "not listed yet" when a company has no published record. |
33
+ | `trooth lint [path]` | Reads the infrastructure the given directory **declares** and prints those declarations as facts, plus a canonical SHA-256 digest of them. Entirely local and offline. |
30
34
  | `trooth --help` / `--version` | Help / version. |
31
35
 
32
- ## Examples
36
+ ## Flags
37
+
38
+ - `--json`: stdout carries exactly one JSON document and nothing else; every
39
+ diagnostic goes to stderr. On an error the document is
40
+ `{"ok": false, "error": "...", "exit": N}`.
41
+
42
+ Any other flag is a usage error (exit 2) that names the flag and lists the
43
+ known ones.
44
+
45
+ ## Exit codes
46
+
47
+ | Code | Meaning |
48
+ | --- | --- |
49
+ | 0 | ok: company listed; lint read at least one declaration; help / version |
50
+ | 1 | finding: company not listed; lint found nothing to read |
51
+ | 2 | usage error: missing argument, unknown flag or command, unreadable path |
52
+ | 3 | Trooth unreachable, non-2xx response, or malformed response |
53
+
54
+ A company that is not listed and a network that could not be read are different
55
+ answers and they exit differently, so a pipeline never reads a Trooth outage as
56
+ "this vendor is unverified".
57
+
58
+ ## `trooth check`
33
59
 
34
60
  ```bash
35
61
  trooth check trooth.co
@@ -38,50 +64,134 @@ trooth check trooth.co
38
64
  ```
39
65
  Trooth Network // witnessed · public · read-only //
40
66
  Trooth, LLC trooth.co
41
- Standing: 91/100 Listed witnessed 2026-08-30
42
-
43
- By discipline
44
- Legal ██████████ 20/20
45
- Security ████████░░ 16/20
46
- AI governance ██████████ 19/20
47
- Business █████████░ 18/20
48
- IP █████████░ 18/20
67
+ Standing: listed and witnessed witnessed 2026-08-30 first published 2026-08-01
49
68
 
50
69
  Live probes 64/65 Attestations 27/35
51
- Badge bronze_rw_... Scan rw_...
70
+ Probes are checks Trooth read for itself. Attestations are the company's own declarations.
71
+ They are counts, reported apart on purpose. Trooth never adds them up into one number.
72
+ Badge bronze_rw_... Key ed25519-2026-01
52
73
 
53
- A witnessed, point-in-time reading of public evidence. Not a certification.
74
+ Recent witness events
75
+ • 2026-08-30 rewitnessed 64 of 65 live probes re-run
76
+
77
+ A witnessed, point-in-time reading of public evidence. Not a certification. Not one number.
78
+ Full record: https://trooth.co/network/trooth.co · Signing keys: https://api.trooth.co/public/keys
54
79
  ```
55
80
 
56
81
  ```bash
57
- # For scripting: raw JSON, exit code 1 when a company is not listed:
58
- trooth check acme.com --json
82
+ # For scripting: one JSON document on stdout, exit 1 when a company is not listed.
83
+ trooth check trooth.co --json
59
84
  ```
60
85
 
61
- ## Flags
86
+ ```json
87
+ {
88
+ "domain": "trooth.co",
89
+ "listed": true,
90
+ "company_name": "Trooth, LLC",
91
+ "witnessed_at": "2026-08-30T00:00:00Z",
92
+ "first_published_at": "2026-08-01T00:00:00Z",
93
+ "badge_id": "bronze_rw_...",
94
+ "probes": { "passed": 64, "total": 65 },
95
+ "attested": { "passed": 27, "total": 35 },
96
+ "events": [ { "type": "rewitnessed", "at": "2026-08-30T00:00:00Z", "detail": "64 of 65 live probes re-run" } ],
97
+ "receipt_signature": "...",
98
+ "authority_key_id": "ed25519-2026-01",
99
+ "verify_keys": "https://api.trooth.co/public/keys",
100
+ "record_url": "https://trooth.co/network/trooth.co"
101
+ }
102
+ ```
62
103
 
63
- - `--json`: print the raw JSON record instead of the formatted view.
64
- - `--strict`: scan only, exit non-zero if there are findings (default: exit 0, advisory).
104
+ Those are the only fields `check --json` emits. Anything else the feed happens
105
+ to carry is dropped on the way out, so a script written against this shape
106
+ keeps working — and a field that would carry a score, tier, grade, rank or
107
+ rating is dropped no matter what the feed sends.
65
108
 
66
- ## Notes
109
+ ## `trooth lint`
67
110
 
68
- - `check` reads only the **public** witness directory. There is no key and no account, and
69
- it never sends anything about you. It is a read.
70
- - A "not listed" result is not a judgement. It means no witnessed standing has been
71
- published for that domain yet. A company gets witnessed by listing at
72
- <https://trooth.co/get-started>.
73
- - `scan` sends only your **declared plan** to the Pre-Flight API; nothing is written back,
74
- and Trooth never touches live infrastructure.
75
- - Set `TROOTH_API` to point at a different base URL (defaults to `https://api.trooth.co`).
76
- - Requires Node 18+ (uses built-in `fetch`).
111
+ `lint` reads what your infrastructure **declares** and reports it. It does not
112
+ judge it. There is no verdict, no pass mark, no severity and no score, and
113
+ nothing is checked against a named standard or regulation — declaring public
114
+ ingress is not a failing, because a load balancer is supposed to be public.
115
+ What the facts mean is your call.
116
+
117
+ ```bash
118
+ trooth lint ./infra
119
+ ```
120
+
121
+ ```
122
+ trooth lint // local · offline · declarations only //
123
+ ./infra 14 declaration file(s) read
124
+ terraform 11 · kubernetes 3
125
+
126
+ Declared
127
+ Regions and zones eu-west-1, us-east-1
128
+ Storage declarations 9
129
+ of those declaring encryption 9
130
+ Logging declarations 4
131
+ Identity declarations 6
132
+ Open to any address (0.0.0.0/0, ::/0) 1
133
+ Marked public 2
134
+ Inline credential literals 0
135
+
136
+ Digest sha256:a963291330bce2ab…
137
+ ```
138
+
139
+ It reads `.tf`, `.tf.json`, Kubernetes YAML (anything with `apiVersion` and
140
+ `kind`), `terraform show -json` plan files and Dockerfiles. It is a declaration
141
+ reader, not a full HCL parser, and it says so rather than pretending otherwise.
142
+
143
+ **Nothing leaves the machine.** No file name, no line, no code and no value is
144
+ ever printed or transmitted — only counts, resource type names and region
145
+ strings. The credential count is a count: it never shows the literal it found.
146
+
147
+ **The digest** is a SHA-256 over the canonical fact document with the timestamp
148
+ excluded, so the same tree always produces the same digest. Record it in CI, or
149
+ on your Trooth record, as evidence that a given state was observed — without
150
+ publishing the tree it came from.
151
+
152
+ ```bash
153
+ # In CI: keep the fact document as a build artifact.
154
+ trooth lint --json > trooth-attestation.json
155
+ ```
77
156
 
78
157
  ## Use it from an AI assistant
79
158
 
80
- The same public network powers Trooth's read-only MCP server, so ChatGPT, Claude, Cursor,
81
- or any MCP client can ask about a company in plain words:
159
+ The same public network powers Trooth's read-only MCP server, so ChatGPT,
160
+ Claude, Cursor, or any MCP client can ask about a company in plain words:
82
161
 
83
162
  ```
84
163
  https://api.trooth.co/public/mcp
85
164
  ```
86
165
 
166
+ ## Notes
167
+
168
+ - `check` reads only the **public** record. There is no key and no account, and
169
+ it never sends anything about you. It is a read.
170
+ - A "not listed" result is not a judgement. It means no record has been
171
+ published for that domain yet. A company gets one at
172
+ <https://trooth.co/get-started>.
173
+ - Set `TROOTH_API` to point at a different base URL (defaults to
174
+ `https://api.trooth.co`). `lint` ignores it: `lint` makes no requests.
175
+ - Set `NO_COLOR=1` to disable ANSI colour. Colour is already off when stdout is
176
+ not a TTY.
177
+
178
+ ## Changes in 0.4.0
179
+
180
+ Breaking, and deliberately so.
181
+
182
+ - **`scan` and `eu` are removed.** Trooth does not scan infrastructure against a
183
+ standard, does not issue a verdict and does not offer a compliance product.
184
+ Both commands now exit 2 with a sentence saying so, rather than "unknown
185
+ command", so an old CI job or an old README gets an explanation.
186
+ - **`lint` is real.** In 0.3.0 it shelled out to `@trooth/os`, which was never
187
+ published, so the command failed for everyone who ran it. It is now
188
+ implemented in this binary, entirely locally.
189
+ - **`scan_id` is gone from `check --json`.** Every other field is unchanged.
190
+ - **`--strict` is gone**, along with the only command that took it.
191
+ - The package description and keywords no longer name any certification,
192
+ standard or regulation, because Trooth does not offer one.
193
+
194
+ The Trooth Network is Trooth's only product. This CLI, the public API and the
195
+ MCP server are interfaces to it, not separate products.
196
+
87
197
  Trooth automates. Trooth never signs for you.
package/bin/trooth.mjs CHANGED
@@ -2,97 +2,191 @@
2
2
  // Trooth CLI. Command name: `trooth`
3
3
  // Copyright (c) 2026 Trooth, LLC. All rights reserved.
4
4
  //
5
+ // The Trooth Network is one public record per company: identity, products and demos,
6
+ // domain and marketing links, people, documents, security and privacy posture,
7
+ // procurement terms, relationships and sub-processors — each one witnessed, signed
8
+ // and dated. This CLI is the terminal interface to that record.
9
+ //
5
10
  // Commands:
6
- // trooth check <domain> Read a company's witnessed trust standing from the
7
- // public Trooth Network (read-only, no key, no account).
8
- // trooth scan <plan> Send a Terraform plan to Trooth Pre-Flight (advisory, report-only).
9
- // <plan> may be a `terraform show -json` JSON file, OR a binary
10
- // .tfplan (the CLI will run `terraform show -json` for you).
11
- // trooth eu ... Push EU AI Act evidence into Trooth from CI/CD (needs a token).
12
- // trooth lint [path] Local, read-only IaC drift check (never transmits your code).
13
- // trooth --help Show help. trooth --version Show version.
11
+ // trooth check <domain> Read a company's witnessed record from the public Trooth
12
+ // Network. Read-only. No key, no account, nothing sent about you.
13
+ // trooth lint [path] Read the infrastructure THIS repository declares and print
14
+ // the declared facts plus a canonical digest of them.
15
+ // Fully local. Offline. Your source never leaves the machine.
16
+ // trooth --help Show help. trooth --version Show version.
17
+ //
18
+ // WHAT THIS TOOL DOES NOT DO, ON PURPOSE:
19
+ // It does not score, rate, rank, grade or tier a company or a repository.
20
+ // It does not check anything against a named certification, standard or regulation.
21
+ // It does not produce a verdict, a pass mark or a percentage.
22
+ // It publishes facts and counts, reported apart, and never adds them into one number.
23
+ //
24
+ // Exit codes (stable, for scripts):
25
+ // 0 ok (listed; lint read at least one declaration; help/version)
26
+ // 1 finding (domain not listed; lint found nothing to read)
27
+ // 2 usage error (missing argument, unknown flag or command, unreadable path)
28
+ // 3 network/upstream error (Trooth unreachable, non-2xx, malformed response)
14
29
  //
15
- // `check` reads only PUBLIC, already-published witness records. `scan` is ADVISORY and
16
- // REPORT-ONLY: Trooth never applies changes to your infrastructure, and the scan posts
17
- // only your declared plan. Trooth automates. Trooth never signs for you.
30
+ // With --json, stdout carries exactly one JSON document and nothing else. Every
31
+ // diagnostic goes to stderr.
18
32
 
19
- import { readFileSync, existsSync } from 'node:fs';
20
- import { execFileSync } from 'node:child_process';
33
+ import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
34
+ import { createHash } from 'node:crypto';
21
35
  import { createRequire } from 'node:module';
36
+ import { join, relative, extname, basename } from 'node:path';
22
37
 
23
38
  const API = process.env.TROOTH_API || 'https://api.trooth.co';
24
- const J='\x1b[32m', D='\x1b[2m', B='\x1b[1m', R='\x1b[31m', A='\x1b[33m', C='\x1b[36m', X='\x1b[0m';
25
39
  const require = createRequire(import.meta.url);
26
- let VERSION = '0.2.0';
40
+ let VERSION = '0.4.0';
27
41
  try { VERSION = require('../package.json').version; } catch {}
28
42
 
43
+ const EXIT = { OK: 0, FINDING: 1, USAGE: 2, UPSTREAM: 3 };
44
+
45
+ // Colour only when stdout is a TTY and NO_COLOR is unset, so piped output is clean.
46
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
47
+ const c = (code) => (useColor ? code : '');
48
+ const J = c('\x1b[32m'), D = c('\x1b[2m'), B = c('\x1b[1m'), R = c('\x1b[31m'), A = c('\x1b[33m'), C = c('\x1b[36m'), X = c('\x1b[0m');
49
+
29
50
  const argv = process.argv.slice(2);
30
51
  const cmd = argv[0];
52
+ const asJson = argv.includes('--json');
53
+
54
+ /* ------------------------------------------------------------- output ---- */
55
+
56
+ function out(s) { process.stdout.write(s + '\n'); }
57
+ function diag(s) { process.stderr.write(s + '\n'); }
58
+ function emitJson(obj) { out(JSON.stringify(obj, null, 2)); }
59
+
60
+ function fail(code, message, extra = {}) {
61
+ diag(`${R}error${X} ${message}`);
62
+ if (asJson) emitJson({ ok: false, error: message, exit: code, ...extra });
63
+ process.exit(code);
64
+ }
65
+
66
+ /** Keys that would carry a score, tier, grade, rank, rating or percentage. Nothing
67
+ * under these names is ever printed, in any mode, no matter what an upstream feed
68
+ * sends. `rate` is matched only as a whole key so keys like generatedAt survive. */
69
+ const BANNED_KEY = /score|tier|grade|rank|rating|level|percent|^rate$/i;
70
+
71
+ function scrub(value) {
72
+ if (Array.isArray(value)) return value.map(scrub);
73
+ if (value && typeof value === 'object') {
74
+ const clean = {};
75
+ for (const [k, v] of Object.entries(value)) {
76
+ if (BANNED_KEY.test(k)) continue;
77
+ clean[k] = scrub(v);
78
+ }
79
+ return clean;
80
+ }
81
+ return value;
82
+ }
83
+
84
+ /* --------------------------------------------------------------- args ---- */
85
+
86
+ const FLAGS = {
87
+ check: { bool: ['--json'], value: [] },
88
+ lint: { bool: ['--json'], value: [] },
89
+ };
90
+
91
+ /** Commands that existed in an earlier release and are gone. Naming them explicitly
92
+ * means an old README, an old CI job or an old blog post gets a sentence that says
93
+ * what happened, instead of "unknown command". */
94
+ const RETIRED = {
95
+ scan: 'Trooth does not scan infrastructure against a standard and does not issue a verdict. `trooth lint` reads what your infrastructure declares and prints those facts, locally.',
96
+ eu: 'Trooth does not ingest regulation-specific evidence. Publish evidence on your company record at https://trooth.co/dashboard.',
97
+ preflight: 'Retired. `trooth lint` reads declared infrastructure facts locally instead.',
98
+ };
99
+
100
+ function parseArgs(command) {
101
+ const spec = FLAGS[command];
102
+ const flags = {};
103
+ const positional = [];
104
+ for (let i = 1; i < argv.length; i++) {
105
+ const a = argv[i];
106
+ if (a === '--') { positional.push(...argv.slice(i + 1)); break; }
107
+ if (a.startsWith('--')) {
108
+ const [name, inlineVal] = a.split('=', 2);
109
+ if (spec.bool.includes(name)) { flags[name] = true; continue; }
110
+ if (spec.value.includes(name)) {
111
+ const v = inlineVal !== undefined ? inlineVal : argv[++i];
112
+ if (v === undefined || v.startsWith('--')) fail(EXIT.USAGE, `${name} needs a value.`);
113
+ flags[name] = v; continue;
114
+ }
115
+ const known = [...spec.bool, ...spec.value];
116
+ fail(EXIT.USAGE, `unknown flag ${name} for \`trooth ${command}\`. ` +
117
+ (known.length ? `Known flags: ${known.join(', ')}.` : `\`trooth ${command}\` takes no flags.`) +
118
+ ` Run \`trooth --help\`.`);
119
+ } else if (a.startsWith('-') && a.length > 1) {
120
+ fail(EXIT.USAGE, `unknown flag ${a} for \`trooth ${command}\`. Run \`trooth --help\`.`);
121
+ } else {
122
+ positional.push(a);
123
+ }
124
+ }
125
+ return { flags, positional };
126
+ }
31
127
 
32
- function help() {
33
- console.log(`
34
- ${J}${B}trooth${X} ${D}v${VERSION} · the trust network your terminal can check${X}
128
+ /* --------------------------------------------------------------- help ---- */
129
+
130
+ function helpText() {
131
+ return `
132
+ ${J}${B}trooth${X} ${D}v${VERSION} · the terminal interface to the Trooth Network${X}
35
133
 
36
134
  ${B}Usage${X}
37
- trooth check <domain> Read a company's witnessed standing on the Trooth Network
38
- trooth scan <plan> Scan a Terraform plan via Trooth Pre-Flight (advisory)
39
- trooth lint [path] Local read-only IaC drift check
135
+ trooth check <domain> Read a company's witnessed record on the Trooth Network
136
+ trooth lint [path] Read what your infrastructure declares, locally. Offline.
40
137
  trooth --help | --version
41
138
 
42
139
  ${B}Examples${X}
43
140
  trooth check stripe.com ${D}# read a company's witnessed record${X}
44
- trooth check trooth.co --json ${D}# raw JSON, for scripting${X}
45
- terraform show -json plan.tfplan > plan.json && trooth scan plan.json
141
+ trooth check trooth.co --json ${D}# one JSON document on stdout, for scripting${X}
142
+ trooth lint ./infra ${D}# read declared facts + print a canonical digest${X}
143
+ trooth lint --json >> attestation.json ${D}# the same facts, for a CI artifact${X}
46
144
 
47
145
  ${B}Flags${X}
48
- --json Print raw JSON instead of the formatted view
49
- --strict (scan) exit non-zero if findings exist (default: advisory, exit 0)
146
+ --json machine-readable JSON on stdout; diagnostics on stderr
147
+
148
+ ${B}Exit codes${X}
149
+ 0 ok 1 finding (not listed / nothing declared) 2 usage error 3 Trooth unreachable
150
+
151
+ ${D}check reads only public, already-published records. No key, no account.
152
+ lint is entirely local: it opens files, and opens no sockets. Your source never leaves.
153
+ Trooth publishes facts and counts, never one number that sums a company up.
154
+ Trooth automates. Trooth never signs for you.${X}
155
+ `;
156
+ }
157
+
158
+ /* -------------------------------------------------------------- fetch ---- */
50
159
 
51
- ${D}check reads only public, already-published witness records. No key, no account.
52
- scan is advisory and report-only. Trooth automates. Trooth never signs for you.${X}
53
- `);
160
+ async function callTrooth(path, init, what) {
161
+ let res;
162
+ try {
163
+ res = await fetch(`${API}${path}`, {
164
+ ...init,
165
+ headers: { accept: 'application/json', 'user-agent': `trooth-cli/${VERSION}`, ...(init && init.headers) },
166
+ });
167
+ } catch (e) {
168
+ fail(EXIT.UPSTREAM, `could not reach ${what} at ${API}: ${e && e.message ? e.message : e}`);
169
+ }
170
+ if (!res.ok) {
171
+ const body = await res.text().catch(() => '');
172
+ fail(EXIT.UPSTREAM, `${what} returned HTTP ${res.status}.${body ? ' ' + body.slice(0, 300).replace(/\s+/g, ' ') : ''}`, { http_status: res.status });
173
+ }
174
+ try { return await res.json(); }
175
+ catch { fail(EXIT.UPSTREAM, `${what} returned a response that is not JSON.`); }
54
176
  }
55
177
 
56
178
  /* --------------------------------------------------------------- check ---- */
57
179
 
58
- /** Normalize a domain the way the Trooth Network does: strip scheme, path,
59
- * leading www, case, and trailing slash. */
60
180
  function normalizeDomain(input) {
61
181
  if (!input) return '';
62
182
  let s = String(input).trim().toLowerCase();
63
- s = s.replace(/^[a-z]+:\/\//, ''); // scheme
64
- s = s.replace(/\/.*$/, ''); // path/query/hash
65
- s = s.replace(/^www\./, ''); // leading www
66
- s = s.replace(/\.$/, ''); // trailing dot
183
+ s = s.replace(/^[a-z]+:\/\//, '');
184
+ s = s.replace(/\/.*$/, '');
185
+ s = s.replace(/^www\./, '');
186
+ s = s.replace(/\.$/, '');
67
187
  return s;
68
188
  }
69
189
 
70
- const CATEGORY_LABELS = {
71
- legal: 'Legal',
72
- security: 'Security',
73
- ai: 'AI governance',
74
- business: 'Business',
75
- ip: 'IP',
76
- };
77
-
78
- /** Pull a 0..100 score out of the feed's score field (object or number). */
79
- function scorePct(score) {
80
- if (score == null) return null;
81
- if (typeof score === 'number') return Math.round(score);
82
- if (typeof score === 'object') {
83
- if (typeof score.rate === 'number') return Math.round(score.rate * 100);
84
- if (typeof score.passed === 'number' && typeof score.total === 'number' && score.total > 0)
85
- return Math.round((score.passed / score.total) * 100);
86
- }
87
- return null;
88
- }
89
-
90
- function bar(passed, total) {
91
- if (!total) return '';
92
- const filled = Math.round((passed / total) * 10);
93
- return '█'.repeat(filled) + '░'.repeat(10 - filled);
94
- }
95
-
96
190
  function fmtDate(iso) {
97
191
  if (!iso) return '';
98
192
  const d = new Date(iso);
@@ -100,208 +194,328 @@ function fmtDate(iso) {
100
194
  return d.toISOString().slice(0, 10);
101
195
  }
102
196
 
197
+ function count(obj) {
198
+ if (!obj || typeof obj !== 'object') return null;
199
+ if (typeof obj.passed !== 'number' || typeof obj.total !== 'number') return null;
200
+ return { passed: obj.passed, total: obj.total };
201
+ }
202
+
203
+ function projectRecord(v, domain) {
204
+ const events = Array.isArray(v.events)
205
+ ? v.events.filter((e) => e && e.type).map((e) => ({ type: String(e.type), at: e.at, detail: e.detail }))
206
+ : [];
207
+ const rec = {
208
+ domain,
209
+ listed: true,
210
+ company_name: v.company_name || domain,
211
+ witnessed_at: v.passed_at || null,
212
+ first_published_at: v.first_published_at || null,
213
+ badge_id: v.badge_id || null,
214
+ probes: count(v.probes),
215
+ attested: count(v.attested),
216
+ events,
217
+ receipt_signature: v.receipt_signature || null,
218
+ authority_key_id: v.authority_key_id || null,
219
+ verify_keys: `${API}/public/keys`,
220
+ record_url: `https://trooth.co/network/${encodeURIComponent(domain)}`,
221
+ };
222
+ if (v.category) rec.category = String(v.category);
223
+ if (v.description) rec.description = String(v.description);
224
+ return scrub(rec);
225
+ }
226
+
103
227
  async function check() {
104
- const domainArg = argv.find((a, i) => i > 0 && !a.startsWith('--'));
105
- const asJson = argv.includes('--json');
106
- const domain = normalizeDomain(domainArg);
107
- if (!domain) {
108
- console.error(`${R}error${X} missing <domain>. Try: trooth check stripe.com`);
109
- process.exit(2);
110
- }
228
+ const { positional } = parseArgs('check');
229
+ if (positional.length > 1) fail(EXIT.USAGE, `check takes one <domain>, got: ${positional.join(' ')}`);
230
+ const domain = normalizeDomain(positional[0]);
231
+ if (!domain) fail(EXIT.USAGE, 'missing <domain>. Try: trooth check stripe.com');
111
232
 
112
- let res;
113
- try {
114
- res = await fetch(`${API}/directory/api/vendors`, {
115
- headers: { accept: 'application/json', 'user-agent': `trooth-cli/${VERSION}` },
116
- });
117
- } catch (e) {
118
- console.error(`${R}error${X} could not reach the Trooth Network at ${API}: ${e.message}`);
119
- process.exit(1);
120
- }
121
- if (!res.ok) {
122
- console.error(`${R}error${X} the Trooth Network returned HTTP ${res.status}.`);
123
- process.exit(1);
124
- }
125
- let data;
126
- try { data = await res.json(); } catch { data = {}; }
233
+ const data = await callTrooth('/directory/api/vendors', { method: 'GET' }, 'the Trooth Network');
127
234
  const vendors = Array.isArray(data && data.vendors) ? data.vendors : [];
128
235
  const vendor = vendors.find((v) => v && normalizeDomain(v.domain) === domain) || null;
129
236
 
130
- if (asJson) {
131
- console.log(JSON.stringify(vendor || { domain, listed: false }, null, 2));
132
- process.exit(vendor ? 0 : 1);
133
- }
134
-
135
237
  if (!vendor) {
136
- console.log(`\n${B}${domain}${X} ${D}//${X} ${A}not listed on the Trooth Network yet${X}`);
137
- console.log(`\n${D}No witnessed standing has been published for this domain. That is not a`);
138
- console.log(`judgement. It simply has not been witnessed. A company gets witnessed by`);
139
- console.log(`listing at ${X}${C}https://trooth.co/get-started${X}${D}: the scan reads its public surface and`);
140
- console.log(`publishes a signed, dated standing that anyone can check.${X}\n`);
141
- process.exit(1);
142
- }
143
-
144
- const pct = scorePct(vendor.score);
145
- const name = vendor.company_name || domain;
146
- const tier = vendor.tier ? vendor.tier[0].toUpperCase() + vendor.tier.slice(1) : 'Listed';
147
- const when = fmtDate(vendor.passed_at);
148
-
149
- console.log(`\n${J}${B}Trooth Network${X} ${D}// witnessed · public · read-only //${X}`);
150
- console.log(`${B}${name}${X} ${C}${domain}${X}`);
151
- const scoreStr = pct == null ? `${D}not yet scored${X}` : `${B}${pct}/100${X}`;
152
- console.log(`Standing: ${scoreStr} ${J}${tier}${X}${when ? ` ${D}witnessed ${when}${X}` : ''}`);
153
-
154
- // Categories (object keyed by discipline).
155
- const cats = vendor.categories;
156
- if (cats && typeof cats === 'object' && !Array.isArray(cats)) {
157
- console.log(`\n${B}By discipline${X}`);
158
- for (const key of ['legal', 'security', 'ai', 'business', 'ip']) {
159
- const c = cats[key];
160
- if (!c || typeof c.total !== 'number') continue;
161
- const label = (CATEGORY_LABELS[key] || key).padEnd(14);
162
- const r = c.total > 0 ? c.passed / c.total : 0;
163
- const col = r >= 0.9 ? J : r >= 0.5 ? A : R;
164
- console.log(` ${label} ${col}${bar(c.passed, c.total)}${X} ${String(c.passed).padStart(2)}/${c.total}`);
238
+ if (asJson) {
239
+ emitJson({ domain, listed: false, record_url: `https://trooth.co/network/${encodeURIComponent(domain)}` });
240
+ } else {
241
+ out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed on the Trooth Network yet${X}`);
242
+ out(`\n${D}No witnessed record has been published for this domain. That is not a`);
243
+ out(`judgement. It simply has not been witnessed. A company gets a record by`);
244
+ out(`listing at ${X}${C}https://trooth.co/get-started${X}${D}: Trooth reads its public surface and`);
245
+ out(`publishes a signed, dated record that anyone — or any agent — can check.${X}\n`);
165
246
  }
247
+ process.exit(EXIT.FINDING);
166
248
  }
167
249
 
250
+ const rec = projectRecord(vendor, domain);
251
+ if (asJson) { emitJson(rec); process.exit(EXIT.OK); }
252
+
253
+ const when = fmtDate(rec.witnessed_at);
254
+ const since = fmtDate(rec.first_published_at);
255
+ out(`\n${J}${B}Trooth Network${X} ${D}// witnessed · public · read-only //${X}`);
256
+ out(`${B}${rec.company_name}${X} ${C}${domain}${X}`);
257
+ out(`Standing: ${J}listed and witnessed${X}` +
258
+ (when ? ` ${D}witnessed ${when}${X}` : ` ${D}date not published${X}`) +
259
+ (since ? ` ${D}first published ${since}${X}` : ''));
260
+
168
261
  const line = [];
169
- if (vendor.probes && typeof vendor.probes.total === 'number')
170
- line.push(`${B}Live probes${X} ${vendor.probes.passed}/${vendor.probes.total}`);
171
- if (vendor.attested && typeof vendor.attested.total === 'number')
172
- line.push(`${B}Attestations${X} ${vendor.attested.passed}/${vendor.attested.total}`);
173
- if (line.length) console.log(`\n${line.join(' ')}`);
262
+ if (rec.probes) line.push(`${B}Live probes${X} ${rec.probes.passed}/${rec.probes.total}`);
263
+ if (rec.attested) line.push(`${B}Attestations${X} ${rec.attested.passed}/${rec.attested.total}`);
264
+ if (line.length) {
265
+ out(`\n${line.join(' ')}`);
266
+ out(`${D}Probes are checks Trooth read for itself. Attestations are the company's own declarations.`);
267
+ out(`They are counts, reported apart on purpose. Trooth never adds them up into one number.${X}`);
268
+ }
174
269
 
175
270
  const ids = [];
176
- if (vendor.badge_id) ids.push(`${D}Badge ${vendor.badge_id}${X}`);
177
- if (vendor.scan_id) ids.push(`${D}Scan ${vendor.scan_id}${X}`);
178
- if (ids.length) console.log(ids.join(' '));
179
-
180
- const events = Array.isArray(vendor.events) ? vendor.events : [];
181
- if (events.length) {
182
- console.log(`\n${B}Recent witness events${X}`);
183
- for (const e of events.slice(0, 3)) {
184
- if (!e || !e.type) continue;
185
- console.log(` ${J}•${X} ${D}${fmtDate(e.at)}${X} ${e.type.replace(/_/g, ' ')}${e.detail ? `${D} ${e.detail}${X}` : ''}`);
271
+ if (rec.badge_id) ids.push(`${D}Badge ${rec.badge_id}${X}`);
272
+ if (rec.authority_key_id) ids.push(`${D}Key ${rec.authority_key_id}${X}`);
273
+ if (ids.length) out(ids.join(' '));
274
+
275
+ if (rec.events.length) {
276
+ out(`\n${B}Recent witness events${X}`);
277
+ for (const e of rec.events.slice(0, 3)) {
278
+ out(` ${J}•${X} ${D}${fmtDate(e.at)}${X} ${e.type.replace(/_/g, ' ')}${e.detail ? `${D} ${e.detail}${X}` : ''}`);
186
279
  }
187
280
  }
188
281
 
189
- console.log(`\n${D}A witnessed, point-in-time reading of public evidence. Not a certification.`);
190
- console.log(`Full record and directory: ${X}${C}https://trooth.co/network${X}${D} · Signing keys: ${API}/public/keys${X}\n`);
191
- process.exit(0);
282
+ out(`\n${D}A witnessed, point-in-time reading of public evidence. Not a certification. Not one number.`);
283
+ out(`Full record: ${X}${C}${rec.record_url}${X}${D} · Signing keys: ${rec.verify_keys}${X}\n`);
284
+ process.exit(EXIT.OK);
192
285
  }
193
286
 
194
- /* ---------------------------------------------------------------- scan ---- */
287
+ /* ---------------------------------------------------------------- lint ---- */
288
+ /* WHAT lint IS, AND WHAT IT IS CAREFULLY NOT.
289
+ *
290
+ * It reads the infrastructure a repository DECLARES and reports those
291
+ * declarations as facts: how many storage resources declare encryption, which
292
+ * regions appear, how many rules declare exposure to the whole internet. It
293
+ * does not judge them. There is no verdict, no pass mark, no severity, no
294
+ * score, and nothing is checked against a named standard or regulation.
295
+ * Declaring public ingress is not a failing; a load balancer is supposed to be
296
+ * public. What the facts mean is the reader's call.
297
+ *
298
+ * It opens files and opens no sockets. Nothing about the repository leaves the
299
+ * machine. The digest at the end is a SHA-256 over the canonical fact document
300
+ * with the timestamp excluded, so the same tree always produces the same digest
301
+ * and you can record it as evidence that a given state was observed, without
302
+ * publishing the tree it came from.
303
+ *
304
+ * It is a declaration reader, not an HCL parser: .tf.json, Kubernetes YAML and
305
+ * terraform plan JSON are parsed properly, and .tf files are read at the
306
+ * attribute level. That limit is stated in the output rather than hidden. */
307
+
308
+ const SKIP_DIRS = new Set([
309
+ 'node_modules', '.git', '.terraform', '.next', 'dist', 'build', 'vendor',
310
+ '.venv', 'venv', '__pycache__', '.cache', 'coverage', '.turbo',
311
+ ]);
312
+ const MAX_FILES = 5000;
313
+ const MAX_BYTES = 4 * 1024 * 1024;
314
+
315
+ function walk(root) {
316
+ const found = [];
317
+ const stack = [root];
318
+ let st;
319
+ try { st = statSync(root); } catch { return found; }
320
+ if (st.isFile()) return [root];
321
+ while (stack.length && found.length < MAX_FILES) {
322
+ const dir = stack.pop();
323
+ let entries;
324
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
325
+ for (const e of entries) {
326
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) stack.push(join(dir, e.name)); continue; }
327
+ if (!e.isFile()) continue;
328
+ const n = e.name.toLowerCase();
329
+ const ext = extname(n);
330
+ const keep =
331
+ ext === '.tf' || n.endsWith('.tf.json') ||
332
+ ext === '.yaml' || ext === '.yml' || ext === '.json' ||
333
+ n === 'dockerfile' || n.startsWith('dockerfile.');
334
+ if (keep) found.push(join(dir, e.name));
335
+ if (found.length >= MAX_FILES) break;
336
+ }
337
+ }
338
+ return found;
339
+ }
195
340
 
196
- function loadPlan(file) {
197
- if (!file) { console.error(`${R}error${X} missing <plan> argument. Try: trooth scan ./plan.json`); process.exit(2); }
198
- if (!existsSync(file)) { console.error(`${R}error${X} file not found: ${file}`); process.exit(2); }
199
- // Try to read as JSON (a `terraform show -json` document).
200
- let raw = '';
201
- try { raw = readFileSync(file, 'utf8'); } catch (e) { console.error(`${R}error${X} cannot read ${file}: ${e.message}`); process.exit(2); }
202
- try { return JSON.parse(raw); } catch { /* not JSON, likely a binary .tfplan */ }
203
- // Fall back: ask Terraform to render the plan as JSON.
204
- try {
205
- const out = execFileSync('terraform', ['show', '-json', file], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
206
- return JSON.parse(out);
207
- } catch (e) {
208
- console.error(`${R}error${X} ${file} is not JSON, and \`terraform show -json\` failed.`);
209
- console.error(`${D} Generate JSON first: terraform show -json ${file} > plan.json then trooth scan plan.json${X}`);
210
- process.exit(2);
341
+ const REGION_RE = /\b(?:region|location|availability_zone|aws_region)\s*[:=]\s*["']([A-Za-z0-9][A-Za-z0-9._-]{2,40})["']/g;
342
+ const RESOURCE_RE = /\bresource\s+"([a-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
343
+ const ENCRYPT_RE = /(?:encrypted|encryption|kms_key|sse_algorithm|server_side_encryption|encrypt_at_rest)/i;
344
+ // Classification runs on the RESOURCE TYPE TOKEN (aws_db_instance, not the body
345
+ // text), matched as a substring. An earlier version wrapped these in \b, which
346
+ // never fires inside a snake_case identifier: \bdynamodb\b cannot match
347
+ // aws_dynamodb_table because the underscore either side is a word character, so
348
+ // every storage resource whose name was not the bare word "bucket" went
349
+ // uncounted. Substring matching on the type is both simpler and correct.
350
+ const STORAGE_TYPE = /(bucket|storage|blob|disk|volume|filestore|_db|database|rds|dynamodb|_sql|redis|memcache|elasticache|efs|fileshare|cosmos|bigtable|spanner)/i;
351
+ const LOGGING_TYPE = /(log|trail|audit|monitor|insight|diagnostic)/i;
352
+ const IDENTITY_TYPE = /(iam|role|policy|service_account|serviceaccount|rbac|identity|access_key|keyring|secret)/i;
353
+ const OPEN_CIDR_RE = /(?:"0\.0\.0\.0\/0"|'0\.0\.0\.0\/0'|"::\/0"|'::\/0')/;
354
+ const PUBLIC_RE = /\b(?:publicly_accessible\s*[:=]\s*true|acl\s*[:=]\s*["']public-read|public_network_access_enabled\s*[:=]\s*true|type\s*:\s*LoadBalancer|type\s*:\s*NodePort)\b/;
355
+ // A literal that looks like a credential sitting in the file. Reported as a
356
+ // count only: no file name, no line, and never the value itself.
357
+ const SECRET_RE = /\b(?:password|secret|api[_-]?key|access[_-]?key|token|private[_-]?key)\s*[:=]\s*["'][^"'${}\n]{8,}["']/i;
358
+
359
+ function classify(file, text) {
360
+ const n = basename(file).toLowerCase();
361
+ if (n.endsWith('.tf')) return 'terraform';
362
+ if (n.endsWith('.tf.json')) return 'terraform';
363
+ if (n === 'dockerfile' || n.startsWith('dockerfile.')) return 'container';
364
+ if (n.endsWith('.yaml') || n.endsWith('.yml')) {
365
+ return /^\s*apiVersion\s*:/m.test(text) && /^\s*kind\s*:/m.test(text) ? 'kubernetes' : null;
366
+ }
367
+ if (n.endsWith('.json')) {
368
+ return /"terraform_version"\s*:/.test(text) &&
369
+ (/"planned_values"\s*:/.test(text) || /"resource_changes"\s*:/.test(text)) ? 'terraform-plan' : null;
211
370
  }
371
+ return null;
212
372
  }
213
373
 
214
- async function scan() {
215
- const file = argv.find((a, i) => i > 0 && !a.startsWith('--'));
216
- const strict = argv.includes('--strict');
217
- const asJson = argv.includes('--json');
218
- const plan = loadPlan(file);
374
+ /** Canonical JSON: object keys sorted at every depth, so the digest depends on
375
+ * the facts and not on the order a walk happened to produce them. */
376
+ function canonical(v) {
377
+ if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']';
378
+ if (v && typeof v === 'object') {
379
+ return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}';
380
+ }
381
+ return JSON.stringify(v === undefined ? null : v);
382
+ }
219
383
 
220
- let res;
221
- try {
222
- res = await fetch(`${API}/v1/preflight`, {
223
- method: 'POST',
224
- headers: { 'content-type': 'application/json', 'user-agent': `trooth-cli/${VERSION}` },
225
- body: JSON.stringify({ plan }),
226
- });
227
- } catch (e) {
228
- console.error(`${R}error${X} could not reach Trooth Pre-Flight at ${API}: ${e.message}`);
229
- process.exit(1);
384
+ function lint() {
385
+ const { positional } = parseArgs('lint');
386
+ if (positional.length > 1) fail(EXIT.USAGE, `lint takes one optional [path], got: ${positional.join(' ')}`);
387
+ const target = positional[0] || '.';
388
+ if (!existsSync(target)) fail(EXIT.USAGE, `path not found: ${target}`);
389
+
390
+ const files = walk(target);
391
+ const byKind = { terraform: 0, 'terraform-plan': 0, kubernetes: 0, container: 0 };
392
+ const regions = new Set();
393
+ const resourceTypes = new Map();
394
+ let read = 0, storage = 0, storageEncrypted = 0, logging = 0, identity = 0;
395
+ let openIngress = 0, publicAccess = 0, inlineCredentials = 0;
396
+
397
+ for (const f of files) {
398
+ let text;
399
+ try {
400
+ if (statSync(f).size > MAX_BYTES) continue;
401
+ text = readFileSync(f, 'utf8');
402
+ } catch { continue; }
403
+ const kind = classify(f, text);
404
+ if (!kind) continue;
405
+ byKind[kind]++;
406
+ read++;
407
+
408
+ for (const m of text.matchAll(REGION_RE)) regions.add(m[1]);
409
+
410
+ if (kind === 'terraform') {
411
+ for (const m of text.matchAll(RESOURCE_RE)) {
412
+ resourceTypes.set(m[1], (resourceTypes.get(m[1]) || 0) + 1);
413
+ }
414
+ }
415
+
416
+ // Terraform is split on top-level resource blocks so an attribute in one
417
+ // resource is never credited to another; every other kind is read whole.
418
+ const blocks = kind === 'terraform' ? text.split(/\n(?=resource\s+")/) : [text];
419
+ for (const b of blocks) {
420
+ const header = b.match(/^resource\s+"([a-z0-9_]+)"/);
421
+ const subject = header ? header[1] : b;
422
+ if (STORAGE_TYPE.test(subject)) { storage++; if (ENCRYPT_RE.test(b)) storageEncrypted++; }
423
+ if (LOGGING_TYPE.test(subject)) logging++;
424
+ if (IDENTITY_TYPE.test(subject)) identity++;
425
+ if (OPEN_CIDR_RE.test(b)) openIngress++;
426
+ if (PUBLIC_RE.test(b)) publicAccess++;
427
+ }
428
+ for (const line of text.split('\n')) if (SECRET_RE.test(line)) inlineCredentials++;
230
429
  }
231
- if (!res.ok) {
232
- const body = await res.text().catch(() => '');
233
- console.error(`${R}error${X} Pre-Flight returned HTTP ${res.status}. ${body.slice(0, 300)}`);
234
- process.exit(1);
430
+
431
+ if (read === 0) {
432
+ const msg = `no infrastructure declarations found under ${target}.`;
433
+ diag(`${A}nothing to read${X} ${msg}`);
434
+ diag(`${D} lint reads .tf, .tf.json, Kubernetes YAML (apiVersion + kind), terraform plan`);
435
+ diag(` JSON and Dockerfiles. ${files.length} file(s) were opened and none matched.${X}`);
436
+ if (asJson) emitJson({ ok: false, error: msg, exit: EXIT.FINDING, files_opened: files.length });
437
+ process.exit(EXIT.FINDING);
235
438
  }
236
- const delta = await res.json();
237
-
238
- if (asJson) { console.log(JSON.stringify(delta, null, 2)); process.exit(strict && (delta.summary?.fail > 0) ? 1 : 0); }
239
-
240
- const score = delta.score ?? '-';
241
- const verdict = (delta.verdict || 'reviewed').toUpperCase();
242
- const s = delta.summary || {};
243
- console.log(`\n${J}${B}Trooth Pre-Flight${X} ${D}// advisory · report-only //${X}`);
244
- console.log(`Verdict: ${B}${verdict}${X} Score: ${B}${score}${typeof score === 'number' ? '/100' : ''}${X} ` +
245
- `${J}pass ${s.pass ?? 0}${X} · ${A}fail ${s.fail ?? 0}${X} · ${D}n/a ${s.notApplicable ?? 0}${X}`);
246
- const findings = delta.findings || [];
247
- if (findings.length) {
248
- console.log(`\n${B}${findings.length} finding(s):${X}\n`);
249
- for (const f of findings.slice(0, 50)) {
250
- const sev = (f.severity || '').toLowerCase();
251
- const tag = sev === 'critical' || sev === 'high' ? `${R}● ${sev.toUpperCase()}${X}`
252
- : sev === 'medium' ? `${A}● MEDIUM${X}` : `${D}● ${(sev||'LOW').toUpperCase()}${X}`;
253
- console.log(` ${tag} ${B}${f.title || f.message || f.check || 'finding'}${X}`);
254
- if (f.control || f.frameworks) console.log(` ${J}↳${X} ${f.control || (Array.isArray(f.frameworks) ? f.frameworks.join(', ') : f.frameworks)}`);
255
- }
256
- } else {
257
- console.log(`\n${J}✓ No findings in the declared plan.${X}`);
439
+
440
+ const topTypes = [...resourceTypes.entries()]
441
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
442
+ .slice(0, 12)
443
+ .map(([type, n]) => ({ type, count: n }));
444
+
445
+ const facts = {
446
+ declarations_read: read,
447
+ sources: Object.fromEntries(Object.entries(byKind).filter(([, n]) => n > 0)),
448
+ regions_and_zones_declared: [...regions].sort(),
449
+ resource_types: topTypes,
450
+ storage_declarations: storage,
451
+ storage_declaring_encryption: storageEncrypted,
452
+ logging_declarations: logging,
453
+ identity_declarations: identity,
454
+ declarations_open_to_any_address: openIngress,
455
+ declarations_marked_public: publicAccess,
456
+ inline_credential_literals: inlineCredentials,
457
+ };
458
+
459
+ const digest = createHash('sha256').update(canonical(facts)).digest('hex');
460
+ const doc = {
461
+ tool: 'trooth-lint',
462
+ cli_version: VERSION,
463
+ observed_at: new Date().toISOString(),
464
+ root: (() => { const r = relative(process.cwd(), target); return !r ? '.' : r.startsWith('..') ? target : r; })(),
465
+ facts,
466
+ digest: `sha256:${digest}`,
467
+ note: 'Declared facts only. Read locally; nothing was transmitted. No score, no verdict, no assessment against any standard.',
468
+ };
469
+
470
+ if (asJson) { emitJson(doc); process.exit(EXIT.OK); }
471
+
472
+ out(`\n${J}${B}trooth lint${X} ${D}// local · offline · declarations only //${X}`);
473
+ out(`${D}${doc.root} ${read} declaration file(s) read${X}`);
474
+
475
+ const src = Object.entries(facts.sources).map(([k, n]) => `${k} ${n}`).join(' · ');
476
+ if (src) out(`${D}${src}${X}`);
477
+
478
+ out(`\n${B}Declared${X}`);
479
+ const row = (label, value) => out(` ${label.padEnd(40)} ${value}`);
480
+ row('Regions and zones', facts.regions_and_zones_declared.length ? facts.regions_and_zones_declared.join(', ') : `${D}none declared${X}`);
481
+ row('Storage declarations', `${facts.storage_declarations}`);
482
+ row(' of those declaring encryption', `${facts.storage_declaring_encryption}`);
483
+ row('Logging declarations', `${facts.logging_declarations}`);
484
+ row('Identity declarations', `${facts.identity_declarations}`);
485
+ row('Open to any address (0.0.0.0/0, ::/0)', `${facts.declarations_open_to_any_address}`);
486
+ row('Marked public', `${facts.declarations_marked_public}`);
487
+ row('Inline credential literals', `${facts.inline_credential_literals}`);
488
+
489
+ if (topTypes.length) {
490
+ out(`\n${B}Most declared resource types${X}`);
491
+ for (const t of topTypes.slice(0, 6)) out(` ${String(t.count).padStart(4)} ${D}${t.type}${X}`);
258
492
  }
259
- console.log(`\n${D}${delta.disclaimer || 'Advisory. Analyzes declared infrastructure intent, not live production state. Trooth never applies changes.'}${X}\n`);
260
493
 
261
- process.exit(strict && (s.fail > 0) ? 1 : 0); // advisory by default: exit 0
262
- }
494
+ out(`\n${B}Digest${X} ${C}${doc.digest}${X}`);
495
+ out(`${D}A SHA-256 over the facts above, in canonical form, with the timestamp excluded.`);
496
+ out(`The same tree always produces the same digest, so you can record it as evidence`);
497
+ out(`that a state was observed without publishing the tree it came from.${X}`);
263
498
 
264
- /* ------------------------------------------------------------------ eu ---- */
265
- // `trooth eu` pushes EU AI Act evidence into Trooth from CI/CD (automated observability).
266
- // trooth eu --token <t> --datasets ds.json --evals ev.json --drift dr.json
267
- // Each file is a JSON array. Auth: TROOTH_TOKEN env or --token (bearer or x-trooth-company).
268
- async function eu() {
269
- const flag = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; };
270
- const token = flag('--token') || process.env.TROOTH_TOKEN || '';
271
- const company = flag('--company') || process.env.TROOTH_COMPANY || '';
272
- if (!token && !company) { console.error(`${R}error${X} provide --token <session> or --company <id> (or TROOTH_TOKEN / TROOTH_COMPANY env).`); process.exit(2); }
273
- const load = (f) => { if (!f) return undefined; try { return JSON.parse(readFileSync(f, 'utf8')); } catch (e) { console.error(`${R}error${X} cannot read ${f}: ${e.message}`); process.exit(2); } };
274
- const body = { datasets: load(flag('--datasets')), evals: load(flag('--evals')), drift: load(flag('--drift')) };
275
- if (!body.datasets && !body.evals && !body.drift) { console.error(`${R}error${X} provide at least one of --datasets/--evals/--drift <file.json>`); process.exit(2); }
276
- const headers = { 'content-type': 'application/json', 'user-agent': `trooth-cli/${VERSION}` };
277
- if (token) headers['authorization'] = `Bearer ${token}`;
278
- if (company) headers['x-trooth-company'] = company;
279
- let res;
280
- try { res = await fetch(`${API}/v1/eu/ingest`, { method: 'POST', headers, body: JSON.stringify(body) }); }
281
- catch (e) { console.error(`${R}error${X} could not reach Trooth at ${API}: ${e.message}`); process.exit(1); }
282
- if (!res.ok) { console.error(`${R}error${X} HTTP ${res.status}. ${(await res.text().catch(()=> '')).slice(0,300)}`); process.exit(1); }
283
- const out = await res.json();
284
- console.log(`\n${J}${B}Trooth EU AI Act${X} ${D}// evidence ingested //${X}`);
285
- console.log(`datasets ${B}${out.ingested?.datasets ?? 0}${X} · evals ${B}${out.ingested?.evals ?? 0}${X} · drift ${B}${out.ingested?.drift ?? 0}${X}`);
286
- console.log(`\n${D}${out.disclaimer || 'Advisory. Trooth automates, computes, and documents; it never signs.'}${X}\n`);
287
- process.exit(0);
499
+ out(`\n${D}Counts of what the files declare. Not a judgement: a public load balancer is`);
500
+ out(`supposed to be public. Trooth issues no verdict here and checks nothing against`);
501
+ out(`any standard. Nothing left this machine: lint opens files and opens no sockets.`);
502
+ out(`Publish what you choose on your record at ${X}${C}https://trooth.co/dashboard${X}${D}.${X}\n`);
503
+ process.exit(EXIT.OK);
288
504
  }
289
505
 
290
- async function lint() {
291
- // Local, read-only .tf drift check. Reuses the published @trooth/os linter if present,
292
- // otherwise runs a built-in minimal ruleset. Never transmits your code.
293
- const path = argv[1] && !argv[1].startsWith('--') ? argv[1] : '.';
294
- const { spawnSync } = await import('node:child_process');
295
- const r = spawnSync('npx', ['--yes', '@trooth/os', path], { stdio: 'inherit' });
296
- process.exit(r.status ?? 0);
297
- }
506
+ /* ---------------------------------------------------------------- main ---- */
298
507
 
299
508
  (async () => {
300
- if (cmd === '--version' || cmd === '-v') { console.log(VERSION); return; }
301
- if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { help(); return; }
509
+ if (cmd === '--version' || cmd === '-v' || cmd === 'version') { out(VERSION); return; }
510
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { out(helpText()); return; }
302
511
  if (cmd === 'check') return check();
303
- if (cmd === 'scan') return scan();
304
- if (cmd === 'eu') return eu();
305
512
  if (cmd === 'lint') return lint();
306
- console.error(`${R}error${X} unknown command: ${cmd}\n`); help(); process.exit(2);
307
- })();
513
+ if (Object.prototype.hasOwnProperty.call(RETIRED, cmd)) {
514
+ fail(EXIT.USAGE, `\`trooth ${cmd}\` is retired. ${RETIRED[cmd]} Run \`trooth --help\`.`);
515
+ }
516
+ if (cmd.startsWith('-')) fail(EXIT.USAGE, `unknown flag ${cmd}. Run \`trooth --help\`.`);
517
+ diag(helpText());
518
+ fail(EXIT.USAGE, `unknown command: ${cmd}. Commands: check, lint.`);
519
+ })().catch((e) => {
520
+ fail(EXIT.UPSTREAM, `unexpected failure: ${e && e.message ? e.message : e}`);
521
+ });
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "trooth",
3
- "version": "0.2.0",
4
- "description": "Trooth CLI. Check any company's witnessed trust standing from your terminal (read-only, no key), and scan a Terraform plan against SOC 2 / ISO 27001 / GDPR / HIPAA / NIST AI RMF / EU AI Act. Advisory and report-only. Trooth never applies changes.",
3
+ "version": "0.4.0",
4
+ "description": "The Trooth Network from your terminal. Read any company's public, signed record \u2014 identity, products, commercial terms, people, documents, security and privacy posture, AI practices, procurement terms \u2014 with no key and no account. Also reads the infrastructure your own repository declares, locally and offline. No score, no rating, no verdict.",
5
5
  "type": "module",
6
- "bin": "bin/trooth.mjs",
6
+ "bin": {
7
+ "trooth": "bin/trooth.mjs"
8
+ },
7
9
  "files": [
8
10
  "bin/",
9
11
  "README.md"
@@ -13,32 +15,31 @@
13
15
  },
14
16
  "scripts": {
15
17
  "check": "node ./bin/trooth.mjs check",
16
- "scan": "node ./bin/trooth.mjs scan"
18
+ "lint": "node ./bin/trooth.mjs lint"
17
19
  },
18
20
  "license": "MIT",
19
21
  "author": "Trooth, LLC",
20
22
  "homepage": "https://trooth.co",
21
- "repository": {
22
- "type": "git",
23
- "url": "https://github.com/trooth/trooth-cli.git"
23
+ "bugs": {
24
+ "url": "https://trooth.co/contact"
24
25
  },
25
26
  "keywords": [
26
- "trust",
27
- "vendor-trust",
28
- "trust-network",
29
- "company-check",
27
+ "trooth",
28
+ "trooth-network",
29
+ "company-lookup",
30
+ "vendor-lookup",
30
31
  "due-diligence",
32
+ "procurement",
33
+ "trust-network",
34
+ "company-directory",
31
35
  "mcp",
32
36
  "ai-agents",
33
- "compliance",
34
- "soc2",
35
- "iso27001",
36
- "gdpr",
37
- "hipaa",
38
- "nist-ai-rmf",
39
- "eu-ai-act",
37
+ "agent-tools",
38
+ "signed-records",
39
+ "provenance",
40
+ "infrastructure-as-code",
40
41
  "terraform",
41
- "iac",
42
- "trooth"
42
+ "kubernetes",
43
+ "cli"
43
44
  ]
44
45
  }