trooth 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -48
- package/bin/trooth.mjs +258 -202
- package/package.json +18 -20
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# trooth
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
**`
|
|
10
|
-
**Trooth publishes facts and counts
|
|
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.**
|
|
11
14
|
|
|
12
15
|
## Install / run
|
|
13
16
|
|
|
@@ -20,38 +23,39 @@ npm i -g trooth
|
|
|
20
23
|
trooth check trooth.co
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
Requires Node 18+ (uses built-in `fetch`).
|
|
26
|
+
Requires Node 18+ (uses built-in `fetch`). Zero dependencies.
|
|
24
27
|
|
|
25
28
|
## Commands
|
|
26
29
|
|
|
27
30
|
| Command | What it does |
|
|
28
31
|
| --- | --- |
|
|
29
|
-
| `trooth check <domain>` | Reads a company's
|
|
30
|
-
| `trooth
|
|
31
|
-
| `trooth eu ...` | Pushes EU AI Act evidence (`--datasets`, `--evals`, `--drift`, each a JSON file) into Trooth from CI/CD. Needs `--token` / `TROOTH_TOKEN` or `--company` / `TROOTH_COMPANY`. |
|
|
32
|
-
| `trooth lint [path]` | Local, read-only IaC drift check that runs the `@trooth/os` linter if it is installed. Never transmits your code. `@trooth/os` is not yet on the npm registry, so today this exits 3 with a message saying exactly that. |
|
|
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. |
|
|
33
34
|
| `trooth --help` / `--version` | Help / version. |
|
|
34
35
|
|
|
35
36
|
## Flags
|
|
36
37
|
|
|
37
|
-
- `--json
|
|
38
|
-
|
|
38
|
+
- `--json`: stdout carries exactly one JSON document and nothing else; every
|
|
39
|
+
diagnostic goes to stderr. On an error the document is
|
|
39
40
|
`{"ok": false, "error": "...", "exit": N}`.
|
|
40
|
-
- `--strict` (`scan` only): exit 1 if any check fails. Default is advisory: exit 0.
|
|
41
41
|
|
|
42
|
-
Any other flag is a usage error (exit 2) that names the flag and lists the
|
|
43
|
-
|
|
42
|
+
Any other flag is a usage error (exit 2) that names the flag and lists the
|
|
43
|
+
known ones.
|
|
44
44
|
|
|
45
45
|
## Exit codes
|
|
46
46
|
|
|
47
47
|
| Code | Meaning |
|
|
48
48
|
| --- | --- |
|
|
49
|
-
| 0 | ok: company listed;
|
|
50
|
-
| 1 | finding: company not listed;
|
|
51
|
-
| 2 | usage error: missing argument, unknown flag or command, unreadable
|
|
52
|
-
| 3 | Trooth unreachable, non-2xx response, malformed response
|
|
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".
|
|
53
57
|
|
|
54
|
-
##
|
|
58
|
+
## `trooth check`
|
|
55
59
|
|
|
56
60
|
```bash
|
|
57
61
|
trooth check trooth.co
|
|
@@ -65,7 +69,7 @@ Standing: listed and witnessed witnessed 2026-08-30 first published 2026-08-
|
|
|
65
69
|
Live probes 64/65 Attestations 27/35
|
|
66
70
|
Probes are checks Trooth read for itself. Attestations are the company's own declarations.
|
|
67
71
|
They are counts, reported apart on purpose. Trooth never adds them up into one number.
|
|
68
|
-
Badge bronze_rw_...
|
|
72
|
+
Badge bronze_rw_... Key ed25519-2026-01
|
|
69
73
|
|
|
70
74
|
Recent witness events
|
|
71
75
|
• 2026-08-30 rewitnessed 64 of 65 live probes re-run
|
|
@@ -87,7 +91,6 @@ trooth check trooth.co --json
|
|
|
87
91
|
"witnessed_at": "2026-08-30T00:00:00Z",
|
|
88
92
|
"first_published_at": "2026-08-01T00:00:00Z",
|
|
89
93
|
"badge_id": "bronze_rw_...",
|
|
90
|
-
"scan_id": "rw_...",
|
|
91
94
|
"probes": { "passed": 64, "total": 65 },
|
|
92
95
|
"attested": { "passed": 27, "total": 35 },
|
|
93
96
|
"events": [ { "type": "rewitnessed", "at": "2026-08-30T00:00:00Z", "detail": "64 of 65 live probes re-run" } ],
|
|
@@ -98,46 +101,97 @@ trooth check trooth.co --json
|
|
|
98
101
|
}
|
|
99
102
|
```
|
|
100
103
|
|
|
101
|
-
Those are the only fields `check --json` emits. Anything else the feed happens
|
|
102
|
-
is dropped on the way out, so a script written against this shape
|
|
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.
|
|
108
|
+
|
|
109
|
+
## `trooth lint`
|
|
110
|
+
|
|
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.
|
|
103
116
|
|
|
104
117
|
```bash
|
|
105
|
-
|
|
106
|
-
trooth scan plan.json # advisory, exit 0
|
|
107
|
-
trooth scan plan.json --strict # exit 1 if any check fails
|
|
118
|
+
trooth lint ./infra
|
|
108
119
|
```
|
|
109
120
|
|
|
110
121
|
```
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
```
|
|
115
138
|
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
118
142
|
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
121
146
|
|
|
122
|
-
|
|
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.
|
|
123
151
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<https://trooth.co/get-started>.
|
|
129
|
-
- `scan` sends only your **declared plan** to the Pre-Flight API; nothing is written back,
|
|
130
|
-
and Trooth never touches live infrastructure.
|
|
131
|
-
- Set `TROOTH_API` to point at a different base URL (defaults to `https://api.trooth.co`).
|
|
132
|
-
- Set `NO_COLOR=1` to disable ANSI colour. Colour is already off when stdout is not a TTY.
|
|
152
|
+
```bash
|
|
153
|
+
# In CI: keep the fact document as a build artifact.
|
|
154
|
+
trooth lint --json > trooth-attestation.json
|
|
155
|
+
```
|
|
133
156
|
|
|
134
157
|
## Use it from an AI assistant
|
|
135
158
|
|
|
136
|
-
The same public network powers Trooth's read-only MCP server, so ChatGPT,
|
|
137
|
-
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:
|
|
138
161
|
|
|
139
162
|
```
|
|
140
163
|
https://api.trooth.co/public/mcp
|
|
141
164
|
```
|
|
142
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
|
+
|
|
143
197
|
Trooth automates. Trooth never signs for you.
|
package/bin/trooth.mjs
CHANGED
|
@@ -2,43 +2,42 @@
|
|
|
2
2
|
// Trooth CLI. Command name: `trooth`
|
|
3
3
|
// Copyright (c) 2026 Trooth, LLC. All rights reserved.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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.
|
|
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.
|
|
14
9
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// only
|
|
10
|
+
// Commands:
|
|
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.
|
|
18
17
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// not
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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.
|
|
24
23
|
//
|
|
25
24
|
// Exit codes (stable, for scripts):
|
|
26
|
-
// 0 ok (listed;
|
|
27
|
-
// 1 finding (domain not listed;
|
|
28
|
-
// 2 usage error (missing argument, unknown flag or command, unreadable
|
|
29
|
-
// 3 network/upstream error (Trooth unreachable, non-2xx, malformed response
|
|
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)
|
|
30
29
|
//
|
|
31
30
|
// With --json, stdout carries exactly one JSON document and nothing else. Every
|
|
32
31
|
// diagnostic goes to stderr.
|
|
33
32
|
|
|
34
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
35
|
-
import {
|
|
33
|
+
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
36
35
|
import { createRequire } from 'node:module';
|
|
37
|
-
import {
|
|
36
|
+
import { join, relative, extname, basename } from 'node:path';
|
|
38
37
|
|
|
39
38
|
const API = process.env.TROOTH_API || 'https://api.trooth.co';
|
|
40
39
|
const require = createRequire(import.meta.url);
|
|
41
|
-
let VERSION = '0.
|
|
40
|
+
let VERSION = '0.4.0';
|
|
42
41
|
try { VERSION = require('../package.json').version; } catch {}
|
|
43
42
|
|
|
44
43
|
const EXIT = { OK: 0, FINDING: 1, USAGE: 2, UPSTREAM: 3 };
|
|
@@ -58,21 +57,17 @@ function out(s) { process.stdout.write(s + '\n'); }
|
|
|
58
57
|
function diag(s) { process.stderr.write(s + '\n'); }
|
|
59
58
|
function emitJson(obj) { out(JSON.stringify(obj, null, 2)); }
|
|
60
59
|
|
|
61
|
-
/** Print a diagnostic to stderr and, in --json mode, one machine-readable error
|
|
62
|
-
* document to stdout, then exit with the given code. */
|
|
63
60
|
function fail(code, message, extra = {}) {
|
|
64
61
|
diag(`${R}error${X} ${message}`);
|
|
65
62
|
if (asJson) emitJson({ ok: false, error: message, exit: code, ...extra });
|
|
66
63
|
process.exit(code);
|
|
67
64
|
}
|
|
68
65
|
|
|
69
|
-
/** Keys that would carry a
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* keys like generatedAt survive. */
|
|
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. */
|
|
73
69
|
const BANNED_KEY = /score|tier|grade|rank|rating|level|percent|^rate$/i;
|
|
74
70
|
|
|
75
|
-
/** Recursively drop banned keys from any JSON-ish value. */
|
|
76
71
|
function scrub(value) {
|
|
77
72
|
if (Array.isArray(value)) return value.map(scrub);
|
|
78
73
|
if (value && typeof value === 'object') {
|
|
@@ -90,13 +85,18 @@ function scrub(value) {
|
|
|
90
85
|
|
|
91
86
|
const FLAGS = {
|
|
92
87
|
check: { bool: ['--json'], value: [] },
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.',
|
|
96
98
|
};
|
|
97
99
|
|
|
98
|
-
/** Parse argv[1..] for one command. Unknown flags are a usage error (exit 2), so a
|
|
99
|
-
* typo or a flag from an old README never silently becomes a filename. */
|
|
100
100
|
function parseArgs(command) {
|
|
101
101
|
const spec = FLAGS[command];
|
|
102
102
|
const flags = {};
|
|
@@ -129,38 +129,34 @@ function parseArgs(command) {
|
|
|
129
129
|
|
|
130
130
|
function helpText() {
|
|
131
131
|
return `
|
|
132
|
-
${J}${B}trooth${X} ${D}v${VERSION} · the
|
|
132
|
+
${J}${B}trooth${X} ${D}v${VERSION} · the terminal interface to the Trooth Network${X}
|
|
133
133
|
|
|
134
134
|
${B}Usage${X}
|
|
135
135
|
trooth check <domain> Read a company's witnessed record on the Trooth Network
|
|
136
|
-
trooth
|
|
137
|
-
trooth eu ... Push EU AI Act evidence from CI/CD (needs a token)
|
|
138
|
-
trooth lint [path] Local read-only IaC drift check
|
|
136
|
+
trooth lint [path] Read what your infrastructure declares, locally. Offline.
|
|
139
137
|
trooth --help | --version
|
|
140
138
|
|
|
141
139
|
${B}Examples${X}
|
|
142
140
|
trooth check stripe.com ${D}# read a company's witnessed record${X}
|
|
143
141
|
trooth check trooth.co --json ${D}# one JSON document on stdout, for scripting${X}
|
|
144
|
-
|
|
145
|
-
trooth
|
|
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}
|
|
146
144
|
|
|
147
145
|
${B}Flags${X}
|
|
148
|
-
--json
|
|
149
|
-
--strict (scan) exit 1 if failing checks exist (default: advisory, exit 0)
|
|
146
|
+
--json machine-readable JSON on stdout; diagnostics on stderr
|
|
150
147
|
|
|
151
148
|
${B}Exit codes${X}
|
|
152
|
-
0 ok 1 finding (not listed
|
|
149
|
+
0 ok 1 finding (not listed / nothing declared) 2 usage error 3 Trooth unreachable
|
|
153
150
|
|
|
154
|
-
${D}check reads only public, already-published
|
|
155
|
-
|
|
156
|
-
|
|
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}
|
|
157
155
|
`;
|
|
158
156
|
}
|
|
159
157
|
|
|
160
158
|
/* -------------------------------------------------------------- fetch ---- */
|
|
161
159
|
|
|
162
|
-
/** GET/POST with the CLI user-agent. Any failure to reach Trooth, any non-2xx, and any
|
|
163
|
-
* unparseable body is an upstream error (exit 3), never a finding. */
|
|
164
160
|
async function callTrooth(path, init, what) {
|
|
165
161
|
let res;
|
|
166
162
|
try {
|
|
@@ -181,15 +177,13 @@ async function callTrooth(path, init, what) {
|
|
|
181
177
|
|
|
182
178
|
/* --------------------------------------------------------------- check ---- */
|
|
183
179
|
|
|
184
|
-
/** Normalize a domain the way the Trooth Network does: strip scheme, path,
|
|
185
|
-
* leading www, case, and trailing dot. */
|
|
186
180
|
function normalizeDomain(input) {
|
|
187
181
|
if (!input) return '';
|
|
188
182
|
let s = String(input).trim().toLowerCase();
|
|
189
|
-
s = s.replace(/^[a-z]+:\/\//, '');
|
|
190
|
-
s = s.replace(/\/.*$/, '');
|
|
191
|
-
s = s.replace(/^www\./, '');
|
|
192
|
-
s = s.replace(/\.$/, '');
|
|
183
|
+
s = s.replace(/^[a-z]+:\/\//, '');
|
|
184
|
+
s = s.replace(/\/.*$/, '');
|
|
185
|
+
s = s.replace(/^www\./, '');
|
|
186
|
+
s = s.replace(/\.$/, '');
|
|
193
187
|
return s;
|
|
194
188
|
}
|
|
195
189
|
|
|
@@ -206,9 +200,6 @@ function count(obj) {
|
|
|
206
200
|
return { passed: obj.passed, total: obj.total };
|
|
207
201
|
}
|
|
208
202
|
|
|
209
|
-
/** The public record, projected onto the fields this CLI vouches for. Unknown fields
|
|
210
|
-
* from the feed are not carried, so a field added upstream later cannot reintroduce
|
|
211
|
-
* a number Trooth does not publish. Then scrubbed anyway, belt and braces. */
|
|
212
203
|
function projectRecord(v, domain) {
|
|
213
204
|
const events = Array.isArray(v.events)
|
|
214
205
|
? v.events.filter((e) => e && e.type).map((e) => ({ type: String(e.type), at: e.at, detail: e.detail }))
|
|
@@ -220,7 +211,6 @@ function projectRecord(v, domain) {
|
|
|
220
211
|
witnessed_at: v.passed_at || null,
|
|
221
212
|
first_published_at: v.first_published_at || null,
|
|
222
213
|
badge_id: v.badge_id || null,
|
|
223
|
-
scan_id: v.scan_id || null,
|
|
224
214
|
probes: count(v.probes),
|
|
225
215
|
attested: count(v.attested),
|
|
226
216
|
events,
|
|
@@ -250,9 +240,9 @@ async function check() {
|
|
|
250
240
|
} else {
|
|
251
241
|
out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed on the Trooth Network yet${X}`);
|
|
252
242
|
out(`\n${D}No witnessed record has been published for this domain. That is not a`);
|
|
253
|
-
out(`judgement. It simply has not been witnessed. A company gets
|
|
254
|
-
out(`listing at ${X}${C}https://trooth.co/get-started${X}${D}:
|
|
255
|
-
out(`publishes a signed, dated record that anyone can check.${X}\n`);
|
|
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`);
|
|
256
246
|
}
|
|
257
247
|
process.exit(EXIT.FINDING);
|
|
258
248
|
}
|
|
@@ -279,7 +269,6 @@ async function check() {
|
|
|
279
269
|
|
|
280
270
|
const ids = [];
|
|
281
271
|
if (rec.badge_id) ids.push(`${D}Badge ${rec.badge_id}${X}`);
|
|
282
|
-
if (rec.scan_id) ids.push(`${D}Scan ${rec.scan_id}${X}`);
|
|
283
272
|
if (rec.authority_key_id) ids.push(`${D}Key ${rec.authority_key_id}${X}`);
|
|
284
273
|
if (ids.length) out(ids.join(' '));
|
|
285
274
|
|
|
@@ -295,157 +284,223 @@ async function check() {
|
|
|
295
284
|
process.exit(EXIT.OK);
|
|
296
285
|
}
|
|
297
286
|
|
|
298
|
-
/* ----------------------------------------------------------------
|
|
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
|
+
}
|
|
299
340
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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;
|
|
313
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;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
314
372
|
}
|
|
315
373
|
|
|
316
|
-
/**
|
|
317
|
-
*
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
format: clean.format,
|
|
325
|
-
generatedAt: clean.generatedAt,
|
|
326
|
-
planResourceCount: clean.planResourceCount,
|
|
327
|
-
verdict: clean.verdict || 'reviewed',
|
|
328
|
-
summary: { pass: s.pass ?? 0, fail: s.fail ?? 0, notApplicable: s.notApplicable ?? 0, bySeverity: s.bySeverity },
|
|
329
|
-
findings: Array.isArray(clean.findings) ? clean.findings : [],
|
|
330
|
-
passedChecks: clean.passedChecks,
|
|
331
|
-
notApplicableChecks: clean.notApplicableChecks,
|
|
332
|
-
frameworkCoverage: clean.frameworkCoverage,
|
|
333
|
-
disclaimer: clean.disclaimer || 'Advisory. Analyzes declared infrastructure intent, not live production state. Trooth never applies changes.',
|
|
334
|
-
};
|
|
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);
|
|
335
382
|
}
|
|
336
383
|
|
|
337
|
-
|
|
338
|
-
const {
|
|
339
|
-
if (positional.length > 1) fail(EXIT.USAGE, `
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
for (const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
}
|
|
367
414
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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++;
|
|
429
|
+
}
|
|
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);
|
|
371
438
|
}
|
|
372
|
-
out(`\n${D}${report.disclaimer}${X}\n`);
|
|
373
|
-
process.exit(exitCode);
|
|
374
|
-
}
|
|
375
439
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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,
|
|
389
457
|
};
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
ingested: { datasets: ingested.datasets ?? 0, evals: ingested.evals ?? 0, drift: ingested.drift ?? 0 },
|
|
401
|
-
disclaimer: result.disclaimer || 'Advisory. Trooth automates, computes, and documents; it never signs.',
|
|
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.',
|
|
402
468
|
};
|
|
403
|
-
if (asJson) { emitJson(report); process.exit(EXIT.OK); }
|
|
404
|
-
out(`\n${J}${B}Trooth EU AI Act${X} ${D}// evidence ingested //${X}`);
|
|
405
|
-
out(`datasets ${B}${report.ingested.datasets}${X} · evals ${B}${report.ingested.evals}${X} · drift ${B}${report.ingested.drift}${X}`);
|
|
406
|
-
out(`\n${D}${report.disclaimer}${X}\n`);
|
|
407
|
-
process.exit(EXIT.OK);
|
|
408
|
-
}
|
|
409
469
|
|
|
410
|
-
|
|
470
|
+
if (asJson) { emitJson(doc); process.exit(EXIT.OK); }
|
|
411
471
|
|
|
412
|
-
|
|
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}`);
|
|
413
474
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
* `npx --yes @trooth/os`, and that name is not on the npm registry (404), so every
|
|
417
|
-
* `trooth lint` ended in npx's raw error. */
|
|
418
|
-
function resolveLinter() {
|
|
419
|
-
const roots = [import.meta.url, `file://${process.cwd()}/`];
|
|
420
|
-
for (const root of roots) {
|
|
421
|
-
try {
|
|
422
|
-
const pkgPath = createRequire(root).resolve(`${LINTER_PKG}/package.json`);
|
|
423
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
424
|
-
const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin && Object.values(pkg.bin)[0];
|
|
425
|
-
if (!bin) continue;
|
|
426
|
-
return { dir: dirname(pkgPath), entry: `${dirname(pkgPath)}/${bin}`, version: pkg.version };
|
|
427
|
-
} catch { /* try the next root */ }
|
|
428
|
-
}
|
|
429
|
-
return null;
|
|
430
|
-
}
|
|
475
|
+
const src = Object.entries(facts.sources).map(([k, n]) => `${k} ${n}`).join(' · ');
|
|
476
|
+
if (src) out(`${D}${src}${X}`);
|
|
431
477
|
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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}`);
|
|
437
488
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
diag(`${D} ${LINTER_PKG} is not published on the npm registry and is not installed locally.`);
|
|
442
|
-
diag(` Nothing was linted and nothing left this machine. This CLI does not download it for you.`);
|
|
443
|
-
diag(` When it ships, install it (npm i -D ${LINTER_PKG}) and run \`trooth lint\` again.${X}`);
|
|
444
|
-
process.exit(EXIT.UPSTREAM);
|
|
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}`);
|
|
445
492
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
493
|
+
|
|
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}`);
|
|
498
|
+
|
|
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);
|
|
449
504
|
}
|
|
450
505
|
|
|
451
506
|
/* ---------------------------------------------------------------- main ---- */
|
|
@@ -454,12 +509,13 @@ async function lint() {
|
|
|
454
509
|
if (cmd === '--version' || cmd === '-v' || cmd === 'version') { out(VERSION); return; }
|
|
455
510
|
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { out(helpText()); return; }
|
|
456
511
|
if (cmd === 'check') return check();
|
|
457
|
-
if (cmd === 'scan') return scan();
|
|
458
|
-
if (cmd === 'eu') return eu();
|
|
459
512
|
if (cmd === 'lint') return lint();
|
|
513
|
+
if (Object.prototype.hasOwnProperty.call(RETIRED, cmd)) {
|
|
514
|
+
fail(EXIT.USAGE, `\`trooth ${cmd}\` is retired. ${RETIRED[cmd]} Run \`trooth --help\`.`);
|
|
515
|
+
}
|
|
460
516
|
if (cmd.startsWith('-')) fail(EXIT.USAGE, `unknown flag ${cmd}. Run \`trooth --help\`.`);
|
|
461
517
|
diag(helpText());
|
|
462
|
-
fail(EXIT.USAGE, `unknown command: ${cmd}. Commands: check,
|
|
518
|
+
fail(EXIT.USAGE, `unknown command: ${cmd}. Commands: check, lint.`);
|
|
463
519
|
})().catch((e) => {
|
|
464
520
|
fail(EXIT.UPSTREAM, `unexpected failure: ${e && e.message ? e.message : e}`);
|
|
465
521
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trooth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Trooth
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "The Trooth Network from your terminal. Read any company's public, signed record with no key and no account: identity, products, terms, people, documents, security and AI posture. Also reads what your own repo declares, offline. No score, no verdict.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"trooth": "bin/trooth.mjs"
|
|
@@ -15,33 +15,31 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"check": "node ./bin/trooth.mjs check",
|
|
18
|
-
"
|
|
19
|
-
"test": "node tests/cli.test.mjs"
|
|
18
|
+
"lint": "node ./bin/trooth.mjs lint"
|
|
20
19
|
},
|
|
21
20
|
"license": "MIT",
|
|
22
21
|
"author": "Trooth, LLC",
|
|
23
22
|
"homepage": "https://trooth.co",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"url": "git+https://github.com/trooth/trooth-cli.git"
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://trooth.co/contact"
|
|
27
25
|
},
|
|
28
26
|
"keywords": [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
27
|
+
"trooth",
|
|
28
|
+
"trooth-network",
|
|
29
|
+
"company-lookup",
|
|
30
|
+
"vendor-lookup",
|
|
33
31
|
"due-diligence",
|
|
32
|
+
"procurement",
|
|
33
|
+
"trust-network",
|
|
34
|
+
"company-directory",
|
|
34
35
|
"mcp",
|
|
35
36
|
"ai-agents",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"hipaa",
|
|
41
|
-
"nist-ai-rmf",
|
|
42
|
-
"eu-ai-act",
|
|
37
|
+
"agent-tools",
|
|
38
|
+
"signed-records",
|
|
39
|
+
"provenance",
|
|
40
|
+
"infrastructure-as-code",
|
|
43
41
|
"terraform",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
42
|
+
"kubernetes",
|
|
43
|
+
"cli"
|
|
46
44
|
]
|
|
47
45
|
}
|