trooth 0.4.2 → 0.4.3
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 +124 -93
- package/bin/trooth.mjs +250 -61
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -1,59 +1,56 @@
|
|
|
1
1
|
# trooth
|
|
2
2
|
|
|
3
|
-
The Trooth Network from your terminal.
|
|
3
|
+
The Trooth Network from your terminal. Published on npm as **`trooth`**.
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
5
|
+
Trooth operates the Trooth Network: one public, signed, machine-readable record per company, carrying its identity, products and demos, commercial terms, domain and marketing links, people, documents, security and privacy posture, AI practices, procurement terms and relationships. It is Trooth's only product and it is free.
|
|
10
6
|
|
|
11
|
-
|
|
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.**
|
|
7
|
+
DNS says where a company is. A TLS certificate says the connection is authentic. The Trooth Network says who the company is and what it does with your data.
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
This CLI is the terminal interface to that record. It does two things:
|
|
10
|
+
|
|
11
|
+
- `trooth check <domain>` reads a company's published record from the public Network. No key, no account, and nothing about you is sent beyond what any web request carries (your IP address and a `trooth-cli/<version>` user-agent).
|
|
12
|
+
- `trooth lint [path]` reads what your own infrastructure declares and prints those declarations as facts. Entirely local: it opens files and opens no sockets.
|
|
13
|
+
|
|
14
|
+
**Trooth witnesses and dates facts. It does not grade, rate or rank anyone.** The CLI prints counts, reported apart, and never adds them into one number.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
16
17
|
|
|
17
18
|
```bash
|
|
18
|
-
# No install
|
|
19
|
+
# No install:
|
|
19
20
|
npx trooth check stripe.com
|
|
20
21
|
|
|
21
|
-
#
|
|
22
|
-
npm
|
|
23
|
-
trooth
|
|
22
|
+
# Or install it:
|
|
23
|
+
npm install -g trooth
|
|
24
|
+
trooth --version
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
Node 18 or newer, because the binary uses the built-in `fetch`. Zero dependencies.
|
|
27
28
|
|
|
28
29
|
## Commands
|
|
29
30
|
|
|
30
31
|
| Command | What it does |
|
|
31
|
-
|
|
32
|
-
| `trooth check <domain>` | Reads a company's record from the live
|
|
33
|
-
| `trooth lint [path]` | Reads the infrastructure the given directory
|
|
34
|
-
| `trooth --help`
|
|
32
|
+
|---|---|
|
|
33
|
+
| `trooth check <domain>` | Reads a company's record from the live Network and prints whether it is listed, when it was witnessed and first published, the live-probe and attestation counts, the badge id, the id of the key that signed the record, and the first three events in its ledger. `--json` adds the signature itself. |
|
|
34
|
+
| `trooth lint [path]` | Reads the infrastructure the given directory declares and prints those declarations plus a canonical SHA-256 digest of them. Local and offline. `path` defaults to `.`. |
|
|
35
|
+
| `trooth --help` | Help. Also `-h` and `help`. |
|
|
36
|
+
| `trooth --version` | Version. Also `-v` and `version`. |
|
|
35
37
|
|
|
36
38
|
## Flags
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
diagnostic goes to stderr. On an error the document is
|
|
40
|
-
`{"ok": false, "error": "...", "exit": N}`.
|
|
40
|
+
`--json` is the only flag `check` and `lint` take. With it, stdout carries exactly one JSON document and nothing else, and every diagnostic goes to stderr. On an error the document is `{"ok": false, "error": "...", "exit": N}`; a non-2xx response adds `http_status`, and `lint` with nothing to read adds `files_opened`. `--help` and `--version` print plain text whether or not `--json` is given.
|
|
41
41
|
|
|
42
|
-
Any other flag is a usage error
|
|
43
|
-
known ones.
|
|
42
|
+
Any other flag is a usage error. The message names the flag, and for a `--` flag given to `check` or `lint` it also lists the ones that exist.
|
|
44
43
|
|
|
45
44
|
## Exit codes
|
|
46
45
|
|
|
47
46
|
| Code | Meaning |
|
|
48
|
-
|
|
49
|
-
| 0 |
|
|
50
|
-
| 1 |
|
|
51
|
-
| 2 |
|
|
52
|
-
| 3 | Trooth unreachable, non-2xx response, or
|
|
47
|
+
|---|---|
|
|
48
|
+
| 0 | The company is listed, or `lint` read at least one declaration, or you asked for help or the version. |
|
|
49
|
+
| 1 | The company is not listed, or `lint` found nothing to read. |
|
|
50
|
+
| 2 | Usage error: a missing argument, an unknown flag or command, or a path that does not exist. |
|
|
51
|
+
| 3 | Trooth could not be read: unreachable, a non-2xx response, or a response that is not JSON. An unexpected failure inside the CLI also exits 3. |
|
|
53
52
|
|
|
54
|
-
A company
|
|
55
|
-
answers and they exit differently, so a pipeline never reads a Trooth outage as
|
|
56
|
-
"this vendor is unverified".
|
|
53
|
+
A company with no record and a Network that could not be read are different answers, so they exit differently. A pipeline can tell them apart without parsing prose, and a Trooth outage never reads as a company with no record.
|
|
57
54
|
|
|
58
55
|
## `trooth check`
|
|
59
56
|
|
|
@@ -61,6 +58,8 @@ answers and they exit differently, so a pipeline never reads a Trooth outage as
|
|
|
61
58
|
trooth check trooth.co
|
|
62
59
|
```
|
|
63
60
|
|
|
61
|
+
Example output. The values are illustrative; the shape is what the binary prints.
|
|
62
|
+
|
|
64
63
|
```
|
|
65
64
|
Trooth Network // witnessed · public · read-only //
|
|
66
65
|
Trooth, LLC trooth.co
|
|
@@ -69,17 +68,24 @@ Standing: listed and witnessed witnessed 2026-08-30 first published 2026-08-
|
|
|
69
68
|
Live probes 64/65 Attestations 27/35
|
|
70
69
|
Probes are checks Trooth read for itself. Attestations are the company's own declarations.
|
|
71
70
|
They are counts, reported apart on purpose. Trooth never adds them up into one number.
|
|
72
|
-
Badge
|
|
71
|
+
Badge rw_... Key ed25519-2026-01
|
|
73
72
|
|
|
74
73
|
Recent witness events
|
|
75
|
-
• 2026-08-
|
|
74
|
+
• 2026-08-01 scan completed 64 of 65 live probes passed · 27 of 35 declarations recorded · signature valid
|
|
75
|
+
• 2026-08-01 standing published point-in-time · published to the Trooth Network
|
|
76
|
+
• 2026-08-30 scan completed 64 of 65 live probes passed · 27 of 35 declarations recorded · signature valid
|
|
76
77
|
|
|
77
78
|
A witnessed, point-in-time reading of public evidence. Not a certification. Not one number.
|
|
78
79
|
Full record: https://trooth.co/network/trooth.co · Signing keys: https://api.trooth.co/public/keys
|
|
79
80
|
```
|
|
80
81
|
|
|
82
|
+
The line that begins `Standing:` is the binary's label for the listing state. For a listed company it reads `listed and witnessed`, then the date of the most recent published reading and the date the record was first published. The two counts are live probes (how many returned the expected result, out of how many were read at the last reading) and attestations (how many the company attested, out of how many were asked for).
|
|
83
|
+
|
|
84
|
+
The events section prints the first three entries in the record's ledger. The feed keeps the ledger oldest first, so these are the record's earliest events even though the heading says "Recent"; `--json` carries every event the feed returns. Event types and details are the feed's own wording, printed as received.
|
|
85
|
+
|
|
86
|
+
For scripting:
|
|
87
|
+
|
|
81
88
|
```bash
|
|
82
|
-
# For scripting: one JSON document on stdout, exit 1 when a company is not listed.
|
|
83
89
|
trooth check trooth.co --json
|
|
84
90
|
```
|
|
85
91
|
|
|
@@ -90,10 +96,15 @@ trooth check trooth.co --json
|
|
|
90
96
|
"company_name": "Trooth, LLC",
|
|
91
97
|
"witnessed_at": "2026-08-30T00:00:00Z",
|
|
92
98
|
"first_published_at": "2026-08-01T00:00:00Z",
|
|
93
|
-
"badge_id": "
|
|
99
|
+
"badge_id": "rw_...",
|
|
94
100
|
"probes": { "passed": 64, "total": 65 },
|
|
95
101
|
"attested": { "passed": 27, "total": 35 },
|
|
96
|
-
"events": [
|
|
102
|
+
"events": [
|
|
103
|
+
{ "type": "scan_completed", "at": "2026-08-01T00:00:00Z", "detail": "64 of 65 live probes passed · 27 of 35 declarations recorded · signature valid" },
|
|
104
|
+
{ "type": "standing_published", "at": "2026-08-01T00:00:00Z", "detail": "point-in-time · published to the Trooth Network" },
|
|
105
|
+
{ "type": "scan_completed", "at": "2026-08-30T00:00:00Z", "detail": "64 of 65 live probes passed · 27 of 35 declarations recorded · signature valid" },
|
|
106
|
+
{ "type": "standing_published", "at": "2026-08-30T00:00:00Z", "detail": "point-in-time · published to the Trooth Network" }
|
|
107
|
+
],
|
|
97
108
|
"receipt_signature": "...",
|
|
98
109
|
"authority_key_id": "ed25519-2026-01",
|
|
99
110
|
"verify_keys": "https://api.trooth.co/public/keys",
|
|
@@ -101,26 +112,27 @@ trooth check trooth.co --json
|
|
|
101
112
|
}
|
|
102
113
|
```
|
|
103
114
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
In `probes`, `total` is how many live probes were read at the last reading and `passed` (the API's field name) is how many returned the expected result. In `attested`, `total` is how many declarations were asked for and `passed` is how many the company attested. `witnessed_at` is the date of the most recent published reading. `events` is the whole ledger, oldest first.
|
|
116
|
+
|
|
117
|
+
`category` and `description` are added when the record carries them. Those are the only fields `check --json` emits: anything else the feed happens to carry is dropped on the way out, so a script written against this shape keeps working. A field whose name would carry a score, grade, rank, rating or percentage is dropped no matter what the feed sends.
|
|
118
|
+
|
|
119
|
+
A company with no record exits 1 and emits `{"domain": "...", "listed": false, "record_url": "..."}`. That is not a judgment. It means the Network's public feed carries no record for that domain. A company gets a record at [trooth.co/get-started](https://trooth.co/get-started), free.
|
|
120
|
+
|
|
121
|
+
`receipt_signature` is Trooth's Ed25519 signature and `authority_key_id` names the key that made it; the public keys are listed at [trooth.co/verify/keys](https://trooth.co/verify/keys). You cannot re-run that signature check from this output yet: the exact bytes the signature covers are not published, and this JSON does not carry every field that goes into them. [`trooth-signatures`](https://github.com/troothllc/trooth-signatures) states that gap and what will close it.
|
|
108
122
|
|
|
109
123
|
## `trooth lint`
|
|
110
124
|
|
|
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.
|
|
125
|
+
`lint` reads what your infrastructure **declares** and reports it. It does not judge it. There is no verdict, no pass mark, no severity and no score, and nothing is checked against a named standard or regulation. Declaring public ingress is not a failing: a load balancer is supposed to be public. What the facts mean is your decision.
|
|
116
126
|
|
|
117
127
|
```bash
|
|
118
128
|
trooth lint ./infra
|
|
119
129
|
```
|
|
120
130
|
|
|
131
|
+
Example output. The values are illustrative; the shape is what the binary prints.
|
|
132
|
+
|
|
121
133
|
```
|
|
122
134
|
trooth lint // local · offline · declarations only //
|
|
123
|
-
|
|
135
|
+
infra 14 declaration file(s) read
|
|
124
136
|
terraform 11 · kubernetes 3
|
|
125
137
|
|
|
126
138
|
Declared
|
|
@@ -133,74 +145,93 @@ Declared
|
|
|
133
145
|
Marked public 2
|
|
134
146
|
Inline credential literals 0
|
|
135
147
|
|
|
136
|
-
|
|
148
|
+
Most declared resource types
|
|
149
|
+
5 aws_s3_bucket
|
|
150
|
+
3 aws_iam_role
|
|
151
|
+
2 aws_cloudwatch_log_group
|
|
152
|
+
|
|
153
|
+
Digest sha256:...
|
|
154
|
+
A SHA-256 over the facts above, in canonical form, with the timestamp excluded.
|
|
155
|
+
The same tree always produces the same digest, so you can record it as evidence
|
|
156
|
+
that a state was observed without publishing the tree it came from.
|
|
157
|
+
|
|
158
|
+
Counts of what the files declare. Not a judgement: a public load balancer is
|
|
159
|
+
supposed to be public. Trooth issues no verdict here and checks nothing against
|
|
160
|
+
any standard. Nothing left this machine: lint opens files and opens no sockets.
|
|
161
|
+
Publish what you choose on your record at https://trooth.co/dashboard.
|
|
137
162
|
```
|
|
138
163
|
|
|
139
|
-
It reads `.tf`, `.tf.json`, Kubernetes YAML (anything
|
|
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.
|
|
164
|
+
It reads `.tf`, `.tf.json`, Kubernetes YAML (anything carrying both `apiVersion` and `kind`), `terraform show -json` plan files and Dockerfiles. It is a declaration reader, not a full HCL parser: it matches patterns in the text of each file and parses none of them, so a count can differ from what a full parser would find. Its output does not state this limit.
|
|
142
165
|
|
|
143
|
-
|
|
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.
|
|
166
|
+
Nothing leaves the machine. No file name, no line, no code and no value is printed or transmitted, only the path you gave it, counts, resource type names and region strings. The credential count is a count: the literal it found is never shown.
|
|
146
167
|
|
|
147
|
-
|
|
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.
|
|
168
|
+
The digest is a SHA-256 over the canonical fact document with the timestamp excluded, so the same tree always produces the same digest. Record it in CI, or on your own record, as evidence that a given state was observed, without publishing the tree it came from.
|
|
151
169
|
|
|
152
170
|
```bash
|
|
153
|
-
#
|
|
154
|
-
trooth lint --json > trooth-
|
|
171
|
+
# Keep the fact document as a build artifact.
|
|
172
|
+
trooth lint --json > trooth-lint.json
|
|
155
173
|
```
|
|
156
174
|
|
|
157
|
-
##
|
|
175
|
+
## In GitHub Actions
|
|
158
176
|
|
|
159
|
-
The same
|
|
160
|
-
|
|
177
|
+
The same `lint`, as a step, is [`troothllc/trooth-action`](https://github.com/troothllc/trooth-action). It is advisory by default: it writes what your infrastructure declares to the job summary and does not fail your workflow over anything it read unless you opt in to one of two gates. It does fail the step when it could not read at all: a path that does not exist, or a CLI it could not install or start.
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
- uses: troothllc/trooth-action@v1
|
|
181
|
+
with:
|
|
182
|
+
path: ./infra
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
That repository's README documents the inputs and outputs.
|
|
186
|
+
|
|
187
|
+
## Environment
|
|
188
|
+
|
|
189
|
+
| Variable | Effect |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `TROOTH_API` | Base URL for `check`. Defaults to `https://api.trooth.co`. `lint` ignores it, because `lint` makes no requests. |
|
|
192
|
+
| `NO_COLOR` | Disables ANSI color. Color is already off when stdout is not a TTY. |
|
|
193
|
+
|
|
194
|
+
There is no API key. The binary asks for no credential of any kind and has no write path.
|
|
195
|
+
|
|
196
|
+
## From an AI assistant
|
|
197
|
+
|
|
198
|
+
The same public Network powers Trooth's read-only MCP server, so ChatGPT, Claude, Cursor or any MCP client can ask about a company in plain words:
|
|
161
199
|
|
|
162
200
|
```
|
|
163
201
|
https://api.trooth.co/public/mcp
|
|
164
202
|
```
|
|
165
203
|
|
|
166
|
-
|
|
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.
|
|
204
|
+
Four read-only tools, public data, no key. The pattern is written up at [trooth.co/docs/agents](https://trooth.co/docs/agents).
|
|
177
205
|
|
|
178
|
-
##
|
|
206
|
+
## Removed in 0.4.0
|
|
179
207
|
|
|
180
208
|
Breaking, and deliberately so.
|
|
181
209
|
|
|
182
|
-
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
210
|
+
- The `scan` and `eu` commands are gone. Trooth does not check infrastructure against a standard and does not issue a verdict. Running either exits 2 with a sentence saying what happened, rather than "unknown command", so an old CI job or an old bookmark gets an explanation.
|
|
211
|
+
- `lint` is real. In 0.3.0 it shelled out to a package that was never published, so the command failed for everyone who ran it. It is implemented in this binary now, entirely locally.
|
|
212
|
+
- `scan_id` is gone from `check --json`. Every other field is unchanged.
|
|
213
|
+
- `--strict` is gone, along with the only command that took it.
|
|
214
|
+
- The package description and keywords no longer name any certification, standard or regulation, because Trooth does not offer one.
|
|
215
|
+
|
|
216
|
+
The Trooth Network is Trooth's only product. This CLI, the public API and the MCP server are interfaces to that one record, not separate products.
|
|
193
217
|
|
|
194
|
-
|
|
195
|
-
MCP server are interfaces to it, not separate products.
|
|
218
|
+
## Security
|
|
196
219
|
|
|
197
|
-
|
|
220
|
+
Report a vulnerability through the [Vulnerability Disclosure Policy](https://trooth.co/security/vulnerability-disclosure-policy). Nothing in this CLI takes a credential, so there is no key to leak from it.
|
|
221
|
+
|
|
222
|
+
## Links
|
|
223
|
+
|
|
224
|
+
- The Network: [trooth.co/network](https://trooth.co/network)
|
|
225
|
+
- This CLI on the site: [trooth.co/cli](https://trooth.co/cli)
|
|
226
|
+
- API reference: [trooth.co/docs/api](https://trooth.co/docs/api)
|
|
227
|
+
- Developers: [trooth.co/developers](https://trooth.co/developers)
|
|
228
|
+
- Publish your own record, free: [trooth.co/get-started](https://trooth.co/get-started)
|
|
229
|
+
- Contact: [trooth.co/contact](https://trooth.co/contact)
|
|
198
230
|
|
|
199
231
|
## License
|
|
200
232
|
|
|
201
233
|
Apache License 2.0. See [LICENSE](LICENSE).
|
|
202
234
|
|
|
203
|
-
The same
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
https://trooth.co/contact.
|
|
235
|
+
The same license applies to this repository, to the `trooth` package on npm and to the copyright header in `bin/trooth.mjs`. They are meant to agree. If you find that they do not, that is a defect: please report it at [trooth.co/contact](https://trooth.co/contact).
|
|
236
|
+
|
|
237
|
+
Trooth signs what it witnessed. It never signs on a company's behalf.
|
package/bin/trooth.mjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// Licensed under the Apache License, Version 2.0. See the LICENSE file in this
|
|
5
5
|
// repository, or http://www.apache.org/licenses/LICENSE-2.0
|
|
6
6
|
//
|
|
7
|
-
// The Trooth Network is one public record per company: identity, products and
|
|
8
|
-
// domain and marketing links, people, documents, security and privacy posture,
|
|
9
|
-
// procurement terms, relationships and sub-processors
|
|
10
|
-
//
|
|
7
|
+
// The Trooth Network is one public, signed record per company: identity, products and
|
|
8
|
+
// demos, domain and marketing links, people, documents, security and privacy posture,
|
|
9
|
+
// procurement terms, relationships and sub-processors. Each fact is dated and labeled
|
|
10
|
+
// with where it came from: witnessed, public record, attested or declared. This CLI is
|
|
11
|
+
// the terminal interface to that record.
|
|
11
12
|
//
|
|
12
13
|
// Commands:
|
|
13
14
|
// trooth check <domain> Read a company's witnessed record from the public Trooth
|
|
@@ -18,9 +19,9 @@
|
|
|
18
19
|
// trooth --help Show help. trooth --version Show version.
|
|
19
20
|
//
|
|
20
21
|
// WHAT THIS TOOL DOES NOT DO, ON PURPOSE:
|
|
21
|
-
// It does not
|
|
22
|
-
// It does not check anything against a named
|
|
23
|
-
// It does not produce a verdict, a
|
|
22
|
+
// It does not grade, rate or rank a company or a repository.
|
|
23
|
+
// It does not check anything against a named standard, framework or regulation.
|
|
24
|
+
// It does not produce a verdict, a threshold result or a percentage.
|
|
24
25
|
// It publishes facts and counts, reported apart, and never adds them into one number.
|
|
25
26
|
//
|
|
26
27
|
// Exit codes (stable, for scripts):
|
|
@@ -39,12 +40,12 @@ import { join, relative, extname, basename } from 'node:path';
|
|
|
39
40
|
|
|
40
41
|
const API = process.env.TROOTH_API || 'https://api.trooth.co';
|
|
41
42
|
const require = createRequire(import.meta.url);
|
|
42
|
-
let VERSION = '0.4.
|
|
43
|
+
let VERSION = '0.4.3';
|
|
43
44
|
try { VERSION = require('../package.json').version; } catch {}
|
|
44
45
|
|
|
45
46
|
const EXIT = { OK: 0, FINDING: 1, USAGE: 2, UPSTREAM: 3 };
|
|
46
47
|
|
|
47
|
-
//
|
|
48
|
+
// Color only when stdout is a TTY and NO_COLOR is unset, so piped output is clean.
|
|
48
49
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
49
50
|
const c = (code) => (useColor ? code : '');
|
|
50
51
|
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');
|
|
@@ -94,7 +95,7 @@ const FLAGS = {
|
|
|
94
95
|
* means an old README, an old CI job or an old blog post gets a sentence that says
|
|
95
96
|
* what happened, instead of "unknown command". */
|
|
96
97
|
const RETIRED = {
|
|
97
|
-
scan: 'Trooth does not
|
|
98
|
+
scan: 'Trooth does not check infrastructure against a standard and does not issue a verdict. `trooth lint` reads what your infrastructure declares and prints those facts, locally.',
|
|
98
99
|
eu: 'Trooth does not ingest regulation-specific evidence. Publish evidence on your company record at https://trooth.co/dashboard.',
|
|
99
100
|
preflight: 'Retired. `trooth lint` reads declared infrastructure facts locally instead.',
|
|
100
101
|
};
|
|
@@ -142,7 +143,7 @@ ${B}Examples${X}
|
|
|
142
143
|
trooth check stripe.com ${D}# read a company's witnessed record${X}
|
|
143
144
|
trooth check trooth.co --json ${D}# one JSON document on stdout, for scripting${X}
|
|
144
145
|
trooth lint ./infra ${D}# read declared facts + print a canonical digest${X}
|
|
145
|
-
trooth lint --json
|
|
146
|
+
trooth lint --json > trooth-lint.json ${D}# the same facts as one JSON document, for a CI artifact${X}
|
|
146
147
|
|
|
147
148
|
${B}Flags${X}
|
|
148
149
|
--json machine-readable JSON on stdout; diagnostics on stderr
|
|
@@ -153,7 +154,7 @@ ${B}Exit codes${X}
|
|
|
153
154
|
${D}check reads only public, already-published records. No key, no account.
|
|
154
155
|
lint is entirely local: it opens files, and opens no sockets. Your source never leaves.
|
|
155
156
|
Trooth publishes facts and counts, never one number that sums a company up.
|
|
156
|
-
Trooth
|
|
157
|
+
Trooth signs what it witnessed. It never signs on a company's behalf.${X}
|
|
157
158
|
`;
|
|
158
159
|
}
|
|
159
160
|
|
|
@@ -202,6 +203,37 @@ function count(obj) {
|
|
|
202
203
|
return { passed: obj.passed, total: obj.total };
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
/** The two count lines, in the website's form ("65 read; 64 as expected"). */
|
|
207
|
+
function countLines(rec) {
|
|
208
|
+
const lines = [];
|
|
209
|
+
if (rec.probes) lines.push(`${B}Live probes:${X} ${rec.probes.total} read; ${rec.probes.passed} as expected`);
|
|
210
|
+
if (rec.attested) lines.push(`${B}Self-attestations:${X} ${rec.attested.total} asked; ${rec.attested.passed} attested`);
|
|
211
|
+
return lines;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The feed keeps the ledger oldest first. This returns the `n` most recent
|
|
215
|
+
* events, newest first, ordered by timestamp; events with the same timestamp,
|
|
216
|
+
* or none, keep the feed's order, later entries first. */
|
|
217
|
+
function latestEvents(events, n) {
|
|
218
|
+
const t = (e) => { const v = Date.parse(e.at); return Number.isNaN(v) ? -Infinity : v; };
|
|
219
|
+
return events
|
|
220
|
+
.map((e, i) => ({ e, i }))
|
|
221
|
+
.sort((a, b) => (t(b.e) - t(a.e)) || (b.i - a.i))
|
|
222
|
+
.slice(0, n)
|
|
223
|
+
.map(({ e }) => e);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Display names for the feed's event types. The type strings themselves are
|
|
227
|
+
* identifiers and are printed unchanged in --json. */
|
|
228
|
+
const EVENT_LABELS = {
|
|
229
|
+
scan_completed: 'reading completed',
|
|
230
|
+
standing_published: 'record published to the Trooth Network',
|
|
231
|
+
rewitnessed: 'live probes re-read',
|
|
232
|
+
};
|
|
233
|
+
function eventLabel(type) {
|
|
234
|
+
return Object.prototype.hasOwnProperty.call(EVENT_LABELS, type) ? EVENT_LABELS[type] : type.replace(/_/g, ' ');
|
|
235
|
+
}
|
|
236
|
+
|
|
205
237
|
function projectRecord(v, domain) {
|
|
206
238
|
const events = Array.isArray(v.events)
|
|
207
239
|
? v.events.filter((e) => e && e.type).map((e) => ({ type: String(e.type), at: e.at, detail: e.detail }))
|
|
@@ -240,11 +272,12 @@ async function check() {
|
|
|
240
272
|
if (asJson) {
|
|
241
273
|
emitJson({ domain, listed: false, record_url: `https://trooth.co/network/${encodeURIComponent(domain)}` });
|
|
242
274
|
} else {
|
|
243
|
-
out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed
|
|
244
|
-
out(`\n${D}
|
|
245
|
-
out(`
|
|
246
|
-
out(`
|
|
247
|
-
out(
|
|
275
|
+
out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed in the Trooth Network's public feed${X}`);
|
|
276
|
+
out(`\n${D}The public feed carries no record for this domain. That says nothing about the`);
|
|
277
|
+
out(`company: a domain that never listed, a listing Trooth has not published, and a`);
|
|
278
|
+
out(`record that was revoked all read this way. A company gets a record by listing at`);
|
|
279
|
+
out(`${X}${C}https://trooth.co/get-started${X}${D}: Trooth reads its public surface and publishes`);
|
|
280
|
+
out(`a signed, dated record that anyone, or any agent, can read.${X}\n`);
|
|
248
281
|
}
|
|
249
282
|
process.exit(EXIT.FINDING);
|
|
250
283
|
}
|
|
@@ -254,19 +287,22 @@ async function check() {
|
|
|
254
287
|
|
|
255
288
|
const when = fmtDate(rec.witnessed_at);
|
|
256
289
|
const since = fmtDate(rec.first_published_at);
|
|
257
|
-
out(`\n${J}${B}Trooth Network${X} ${D}//
|
|
290
|
+
out(`\n${J}${B}Trooth Network${X} ${D}// public · signed · read-only //${X}`);
|
|
258
291
|
out(`${B}${rec.company_name}${X} ${C}${domain}${X}`);
|
|
259
|
-
out(`
|
|
260
|
-
(when ? ` ${D}witnessed ${when}${X}` : ` ${D}date not published${X}`) +
|
|
292
|
+
out(`Listing state: ${J}listed and witnessed${X}` +
|
|
293
|
+
(when ? ` ${D}last witnessed ${when}${X}` : ` ${D}date not published${X}`) +
|
|
261
294
|
(since ? ` ${D}first published ${since}${X}` : ''));
|
|
262
295
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
out(
|
|
269
|
-
|
|
296
|
+
// Two counts, never a ratio, in the form the website's record page uses: an
|
|
297
|
+
// "N/M" cell reads as a bar, and a bar reads as a grade. The JSON fields keep
|
|
298
|
+
// the feed's names (`passed`, `total`).
|
|
299
|
+
const counts = countLines(rec);
|
|
300
|
+
if (counts.length) {
|
|
301
|
+
out('');
|
|
302
|
+
for (const l of counts) out(l);
|
|
303
|
+
out(`${D}Live probes are readings Trooth took itself, from the company's public surface.`);
|
|
304
|
+
out(`Self-attestations are what the company attested about itself; Trooth records them`);
|
|
305
|
+
out(`and did not witness them. The two are reported apart and never added into one number.${X}`);
|
|
270
306
|
}
|
|
271
307
|
|
|
272
308
|
const ids = [];
|
|
@@ -274,14 +310,14 @@ async function check() {
|
|
|
274
310
|
if (rec.authority_key_id) ids.push(`${D}Key ${rec.authority_key_id}${X}`);
|
|
275
311
|
if (ids.length) out(ids.join(' '));
|
|
276
312
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
313
|
+
const latest = latestEvents(rec.events, 3);
|
|
314
|
+
if (latest.length) {
|
|
315
|
+
out(`\n${B}Latest ledger events, newest first${X}`);
|
|
316
|
+
for (const e of latest) out(` ${J}•${X} ${D}${fmtDate(e.at)}${X} ${eventLabel(e.type)}`);
|
|
317
|
+
out(`${D} --json carries the whole ledger, with the feed's own wording for each event.${X}`);
|
|
282
318
|
}
|
|
283
319
|
|
|
284
|
-
out(`\n${D}A
|
|
320
|
+
out(`\n${D}A dated, point-in-time record. Trooth issues no verdict and no single number.`);
|
|
285
321
|
out(`Full record: ${X}${C}${rec.record_url}${X}${D} · Signing keys: ${rec.verify_keys}${X}\n`);
|
|
286
322
|
process.exit(EXIT.OK);
|
|
287
323
|
}
|
|
@@ -292,20 +328,31 @@ async function check() {
|
|
|
292
328
|
* It reads the infrastructure a repository DECLARES and reports those
|
|
293
329
|
* declarations as facts: how many storage resources declare encryption, which
|
|
294
330
|
* regions appear, how many rules declare exposure to the whole internet. It
|
|
295
|
-
* does not judge them. There is no verdict, no
|
|
296
|
-
*
|
|
331
|
+
* does not judge them. There is no verdict, no threshold, no severity and no
|
|
332
|
+
* rating, and nothing is checked against a named standard or regulation.
|
|
297
333
|
* Declaring public ingress is not a failing; a load balancer is supposed to be
|
|
298
|
-
* public. What the facts mean is the reader's
|
|
334
|
+
* public. What the facts mean is the reader's decision.
|
|
299
335
|
*
|
|
300
336
|
* It opens files and opens no sockets. Nothing about the repository leaves the
|
|
301
|
-
* machine. The digest at the end is a SHA-256 over the
|
|
302
|
-
*
|
|
303
|
-
*
|
|
337
|
+
* machine. The digest at the end is a SHA-256 over the `facts` object in
|
|
338
|
+
* canonical form (the timestamp, path and CLI version are outside it), so the
|
|
339
|
+
* same tree read by the same CLI version always produces the same digest and
|
|
340
|
+
* you can record it as evidence that a given state was observed, without
|
|
304
341
|
* publishing the tree it came from.
|
|
305
342
|
*
|
|
306
|
-
* It is a
|
|
307
|
-
*
|
|
308
|
-
*
|
|
343
|
+
* HOW EACH SOURCE IS READ. It is a pattern reader, not a Terraform evaluator:
|
|
344
|
+
* variables, modules and for_each are never resolved.
|
|
345
|
+
* .tf regular expressions, split into top-level resource blocks
|
|
346
|
+
* .tf.json parsed as JSON; each resource is one unit
|
|
347
|
+
* plan JSON parsed as JSON (`terraform show -json`); each planned
|
|
348
|
+
* managed resource is one unit
|
|
349
|
+
* Kubernetes YAML regular expressions, one unit per YAML document,
|
|
350
|
+
* classified by its top-level `kind`
|
|
351
|
+
* Dockerfile read for regions, open addresses, public markers and
|
|
352
|
+
* credential literals only; it declares no resource types
|
|
353
|
+
* Storage, logging and identity are classified on a unit's resource type or
|
|
354
|
+
* kind, never on the text around it. The human output prints this list in
|
|
355
|
+
* short, so the limit is stated where the counts are. */
|
|
309
356
|
|
|
310
357
|
const SKIP_DIRS = new Set([
|
|
311
358
|
'node_modules', '.git', '.terraform', '.next', 'dist', 'build', 'vendor',
|
|
@@ -357,6 +404,125 @@ const PUBLIC_RE = /\b(?:publicly_accessible\s*[:=]\s*true|acl\s*[:=]\s*["']publi
|
|
|
357
404
|
// A literal that looks like a credential sitting in the file. Reported as a
|
|
358
405
|
// count only: no file name, no line, and never the value itself.
|
|
359
406
|
const SECRET_RE = /\b(?:password|secret|api[_-]?key|access[_-]?key|token|private[_-]?key)\s*[:=]\s*["'][^"'${}\n]{8,}["']/i;
|
|
407
|
+
// A storage-matching Terraform type that names a SETTING on a store rather than
|
|
408
|
+
// a store: aws_s3_bucket_server_side_encryption_configuration, a bucket policy,
|
|
409
|
+
// a volume attachment, a subnet group. The substring match above catches these
|
|
410
|
+
// (the fixture's one bucket used to count as two storage declarations), so they
|
|
411
|
+
// are not counted as storage. A setting that declares encryption and references
|
|
412
|
+
// a store credits that store with declaring encryption. Applied to snake_case
|
|
413
|
+
// Terraform types only; Kubernetes kinds are CamelCase and are not settings.
|
|
414
|
+
const STORAGE_SETTING = /_(?:configuration|policy|acl|versioning|notification|public_access_block|ownership_controls|attachment|object|item|logging|iam_member|iam_binding|access_point|mount_target|snapshot|subnet_group|parameter_group|option_group)$/;
|
|
415
|
+
const isStorageSetting = (type) => type.includes('_') && STORAGE_SETTING.test(type);
|
|
416
|
+
// An attribute set to false, null or empty declares nothing: `encrypted = false`
|
|
417
|
+
// and `storage_encrypted: false` must not count as declaring encryption.
|
|
418
|
+
const NEGATIVE_ATTR_RE = /^[^\n:=]*[:=]\s*(?:false|null|"false"|'false'|""|''|\[\]|\{\})\s*,?\s*$/gim;
|
|
419
|
+
const declaresEncryption = (body) => ENCRYPT_RE.test(body.replace(NEGATIVE_ATTR_RE, ''));
|
|
420
|
+
|
|
421
|
+
/** JSON text with `"key":` rewritten as `key:`, so the attribute patterns above
|
|
422
|
+
* (written for `key = "v"` and `key: "v"`) read JSON sources too. Escaped
|
|
423
|
+
* quotes inside string values are never rewritten. */
|
|
424
|
+
function flattenJson(text) {
|
|
425
|
+
return text.replace(/"([A-Za-z_][\w.-]*)"\s*:/g, '$1:');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** A parsed JSON value with false, null, empty strings, empty arrays and empty
|
|
429
|
+
* objects removed, so a plan's unset attributes read as absent. */
|
|
430
|
+
function prune(v) {
|
|
431
|
+
if (Array.isArray(v)) { const a = v.map(prune).filter((x) => x !== undefined); return a.length ? a : undefined; }
|
|
432
|
+
if (v && typeof v === 'object') {
|
|
433
|
+
const o = {};
|
|
434
|
+
for (const [k, x] of Object.entries(v)) { const y = prune(x); if (y !== undefined) o[k] = y; }
|
|
435
|
+
return Object.keys(o).length ? o : undefined;
|
|
436
|
+
}
|
|
437
|
+
return v === null || v === false || v === '' ? undefined : v;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
441
|
+
/** A pattern that finds a Terraform address (aws_s3_bucket.logs) in another
|
|
442
|
+
* resource's body, as `aws_s3_bucket.logs.id` or `${aws_s3_bucket.logs.arn}`,
|
|
443
|
+
* and not inside a longer address such as aws_s3_bucket.logs2. */
|
|
444
|
+
const addressRef = (address) => new RegExp(`(?:^|[^\\w.])${escRe(address)}(?![\\w-])`);
|
|
445
|
+
|
|
446
|
+
/** Every resource under `resource` in a .tf.json document, in either of the
|
|
447
|
+
* shapes Terraform's JSON syntax allows (objects, or arrays of objects). */
|
|
448
|
+
function tfJsonResources(doc) {
|
|
449
|
+
const out = [];
|
|
450
|
+
const each = (v, fn) => { if (Array.isArray(v)) v.forEach((x) => each(x, fn)); else if (v && typeof v === 'object') fn(v); };
|
|
451
|
+
each(doc, (top) => each(top.resource, (byType) => {
|
|
452
|
+
for (const [type, byName] of Object.entries(byType)) {
|
|
453
|
+
if (!/^[a-z0-9_]+$/.test(type)) continue;
|
|
454
|
+
each(byName, (names) => { for (const [name, body] of Object.entries(names)) out.push({ type, name, body }); });
|
|
455
|
+
}
|
|
456
|
+
}));
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The managed resources a plan (`terraform show -json`) says will exist:
|
|
461
|
+
* planned_values, every module deep, or resource_changes when a plan has no
|
|
462
|
+
* planned_values. Data sources and deletions are not declarations. */
|
|
463
|
+
function planResources(doc) {
|
|
464
|
+
const out = [];
|
|
465
|
+
const walkModule = (m) => {
|
|
466
|
+
if (!m || typeof m !== 'object') return;
|
|
467
|
+
for (const r of Array.isArray(m.resources) ? m.resources : []) {
|
|
468
|
+
if (r && r.mode !== 'data' && typeof r.type === 'string') out.push({ type: r.type, name: String(r.name ?? ''), body: r.values ?? {} });
|
|
469
|
+
}
|
|
470
|
+
for (const c of Array.isArray(m.child_modules) ? m.child_modules : []) walkModule(c);
|
|
471
|
+
};
|
|
472
|
+
if (doc && doc.planned_values && doc.planned_values.root_module) walkModule(doc.planned_values.root_module);
|
|
473
|
+
else {
|
|
474
|
+
for (const rc of Array.isArray(doc && doc.resource_changes) ? doc.resource_changes : []) {
|
|
475
|
+
const after = rc && rc.change ? rc.change.after : null;
|
|
476
|
+
if (rc && rc.mode !== 'data' && typeof rc.type === 'string' && after) out.push({ type: rc.type, name: String(rc.name ?? ''), body: after });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Split one declaration file into the units lint classifies. Each unit has a
|
|
483
|
+
* `type` (a resource type or Kubernetes kind, or null when there is none),
|
|
484
|
+
* `refs` (patterns another unit's body would contain to refer to this one),
|
|
485
|
+
* `body` (the text attribute patterns run on) and `typed` (whether the type
|
|
486
|
+
* belongs in the resource type list). */
|
|
487
|
+
function unitsOf(kind, file, text) {
|
|
488
|
+
if (kind === 'terraform' && !basename(file).toLowerCase().endsWith('.tf.json')) {
|
|
489
|
+
return text.split(/\n(?=resource\s+")/).map((b) => {
|
|
490
|
+
const h = b.match(/^resource\s+"([a-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/);
|
|
491
|
+
return h ? { type: h[1], refs: [addressRef(`${h[1]}.${h[2]}`)], body: b, typed: false }
|
|
492
|
+
: { type: null, refs: [], body: b, typed: false };
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
if (kind === 'kubernetes') {
|
|
496
|
+
return text.split(/^---[^\n]*$/m).map((d) => {
|
|
497
|
+
const k = d.match(/^kind\s*:\s*["']?([A-Za-z][A-Za-z0-9]*)/m);
|
|
498
|
+
return { type: k ? k[1] : null, refs: [], body: d, typed: false };
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (kind === 'container') return [{ type: null, refs: [], body: text, typed: false }];
|
|
502
|
+
|
|
503
|
+
// .tf.json and plan JSON.
|
|
504
|
+
let doc;
|
|
505
|
+
try { doc = JSON.parse(text); } catch { return [{ type: null, refs: [], body: flattenJson(text), typed: false }]; }
|
|
506
|
+
const asBody = (v) => flattenJson(JSON.stringify(prune(v) ?? {}, null, 1));
|
|
507
|
+
if (kind === 'terraform-plan') {
|
|
508
|
+
return planResources(doc).map((r) => {
|
|
509
|
+
const v = prune(r.body) || {};
|
|
510
|
+
// A plan carries values, not expressions, so a setting names its store
|
|
511
|
+
// by the store's own bucket name or id.
|
|
512
|
+
const refs = ['bucket', 'id'].map((k) => v[k]).filter((x) => typeof x === 'string' && x.length >= 3)
|
|
513
|
+
.map((x) => new RegExp(escRe(JSON.stringify(x))));
|
|
514
|
+
return { type: r.type, refs, body: asBody(r.body), typed: true };
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
const units = tfJsonResources(doc).map((r) => ({ type: r.type, refs: [addressRef(`${r.type}.${r.name}`)], body: asBody(r.body), typed: true }));
|
|
518
|
+
// Everything outside `resource` (providers, variables, locals) is one untyped
|
|
519
|
+
// unit, the way the text before the first resource block is in a .tf file.
|
|
520
|
+
if (doc && typeof doc === 'object' && !Array.isArray(doc)) {
|
|
521
|
+
const rest = { ...doc }; delete rest.resource;
|
|
522
|
+
units.push({ type: null, refs: [], body: asBody(rest), typed: false });
|
|
523
|
+
}
|
|
524
|
+
return units;
|
|
525
|
+
}
|
|
360
526
|
|
|
361
527
|
function classify(file, text) {
|
|
362
528
|
const n = basename(file).toLowerCase();
|
|
@@ -393,7 +559,9 @@ function lint() {
|
|
|
393
559
|
const byKind = { terraform: 0, 'terraform-plan': 0, kubernetes: 0, container: 0 };
|
|
394
560
|
const regions = new Set();
|
|
395
561
|
const resourceTypes = new Map();
|
|
396
|
-
|
|
562
|
+
const stores = []; // { refs, encrypted }, one per storage declaration
|
|
563
|
+
const encryptingSettings = []; // bodies of storage settings that declare encryption
|
|
564
|
+
let read = 0, logging = 0, identity = 0;
|
|
397
565
|
let openIngress = 0, publicAccess = 0, inlineCredentials = 0;
|
|
398
566
|
|
|
399
567
|
for (const f of files) {
|
|
@@ -407,29 +575,44 @@ function lint() {
|
|
|
407
575
|
byKind[kind]++;
|
|
408
576
|
read++;
|
|
409
577
|
|
|
410
|
-
|
|
578
|
+
// JSON sources are read through flattenJson so `"region": "x"` and
|
|
579
|
+
// `"password": "..."` meet the same patterns as HCL and YAML.
|
|
580
|
+
const isJson = extname(f).toLowerCase() === '.json';
|
|
581
|
+
const flat = isJson ? flattenJson(text) : text;
|
|
582
|
+
for (const m of flat.matchAll(REGION_RE)) regions.add(m[1]);
|
|
583
|
+
for (const line of flat.split('\n')) if (SECRET_RE.test(line)) inlineCredentials++;
|
|
411
584
|
|
|
412
|
-
if (kind === 'terraform') {
|
|
585
|
+
if (kind === 'terraform' && !isJson) {
|
|
413
586
|
for (const m of text.matchAll(RESOURCE_RE)) {
|
|
414
587
|
resourceTypes.set(m[1], (resourceTypes.get(m[1]) || 0) + 1);
|
|
415
588
|
}
|
|
416
589
|
}
|
|
417
590
|
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
for (const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
if (STORAGE_TYPE.test(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
591
|
+
// Each unit is classified on its own type, so an attribute in one resource
|
|
592
|
+
// is never credited to another, and a word in a file's text is never taken
|
|
593
|
+
// for a resource type.
|
|
594
|
+
for (const u of unitsOf(kind, f, text)) {
|
|
595
|
+
const t = u.type;
|
|
596
|
+
if (t && u.typed) resourceTypes.set(t, (resourceTypes.get(t) || 0) + 1);
|
|
597
|
+
if (t && STORAGE_TYPE.test(t)) {
|
|
598
|
+
if (isStorageSetting(t)) { if (declaresEncryption(u.body)) encryptingSettings.push(u.body); }
|
|
599
|
+
else stores.push({ refs: u.refs, encrypted: declaresEncryption(u.body) });
|
|
600
|
+
}
|
|
601
|
+
if (t && LOGGING_TYPE.test(t)) logging++;
|
|
602
|
+
if (t && IDENTITY_TYPE.test(t)) identity++;
|
|
603
|
+
if (OPEN_CIDR_RE.test(u.body)) openIngress++;
|
|
604
|
+
if (PUBLIC_RE.test(u.body)) publicAccess++;
|
|
429
605
|
}
|
|
430
|
-
for (const line of text.split('\n')) if (SECRET_RE.test(line)) inlineCredentials++;
|
|
431
606
|
}
|
|
432
607
|
|
|
608
|
+
// A store declares encryption in its own body, or through a setting resource
|
|
609
|
+
// (anywhere in the tree) that declares encryption and refers to it.
|
|
610
|
+
for (const s of stores) {
|
|
611
|
+
if (!s.encrypted && s.refs.some((re) => encryptingSettings.some((b) => re.test(b)))) s.encrypted = true;
|
|
612
|
+
}
|
|
613
|
+
const storage = stores.length;
|
|
614
|
+
const storageEncrypted = stores.filter((s) => s.encrypted).length;
|
|
615
|
+
|
|
433
616
|
if (read === 0) {
|
|
434
617
|
const msg = `no infrastructure declarations found under ${target}.`;
|
|
435
618
|
diag(`${A}nothing to read${X} ${msg}`);
|
|
@@ -466,7 +649,7 @@ function lint() {
|
|
|
466
649
|
root: (() => { const r = relative(process.cwd(), target); return !r ? '.' : r.startsWith('..') ? target : r; })(),
|
|
467
650
|
facts,
|
|
468
651
|
digest: `sha256:${digest}`,
|
|
469
|
-
note: 'Declared facts only. Read locally; nothing was transmitted. No
|
|
652
|
+
note: 'Declared facts only. Read locally; nothing was transmitted. No verdict and no assessment against any standard.',
|
|
470
653
|
};
|
|
471
654
|
|
|
472
655
|
if (asJson) { emitJson(doc); process.exit(EXIT.OK); }
|
|
@@ -495,10 +678,16 @@ function lint() {
|
|
|
495
678
|
|
|
496
679
|
out(`\n${B}Digest${X} ${C}${doc.digest}${X}`);
|
|
497
680
|
out(`${D}A SHA-256 over the facts above, in canonical form, with the timestamp excluded.`);
|
|
498
|
-
out(`The same tree
|
|
499
|
-
out(`that a state was observed without publishing the tree
|
|
681
|
+
out(`The same tree read by the same trooth version produces the same digest, so you can`);
|
|
682
|
+
out(`record it as evidence that a state was observed without publishing the tree.${X}`);
|
|
683
|
+
|
|
684
|
+
out(`\n${B}How this was read${X}`);
|
|
685
|
+
out(`${D} A pattern reader, not a Terraform evaluator: variables and modules are not resolved.`);
|
|
686
|
+
out(` .tf by pattern, one resource block at a time. .tf.json and plan JSON parsed, one`);
|
|
687
|
+
out(` resource at a time. Kubernetes YAML by pattern, one document at a time, by kind.`);
|
|
688
|
+
out(` Dockerfiles for regions, open addresses, public markers and credential literals only.${X}`);
|
|
500
689
|
|
|
501
|
-
out(`\n${D}Counts of what the files declare. Not a
|
|
690
|
+
out(`\n${D}Counts of what the files declare. Not a judgment: a public load balancer is`);
|
|
502
691
|
out(`supposed to be public. Trooth issues no verdict here and checks nothing against`);
|
|
503
692
|
out(`any standard. Nothing left this machine: lint opens files and opens no sockets.`);
|
|
504
693
|
out(`Publish what you choose on your record at ${X}${C}https://trooth.co/dashboard${X}${D}.${X}\n`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trooth",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "The Trooth Network from your terminal. Read
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "The Trooth Network from your terminal. Read a company's public, signed record with no key and no account: listing state, dates, live-probe and self-attestation counts, signing key id. Also reads what your own repo declares, offline. No verdict.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"trooth": "bin/trooth.mjs"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"check": "node ./bin/trooth.mjs check",
|
|
19
19
|
"lint": "node ./bin/trooth.mjs lint",
|
|
20
|
-
"test": "node ./bin/trooth.mjs lint tests/fixtures/infra --json > /dev/null && node ./bin/trooth.mjs --version"
|
|
20
|
+
"test": "node ./bin/trooth.mjs lint tests/fixtures/infra --json > /dev/null && node ./bin/trooth.mjs --version && node tests/action.test.mjs && node tests/lint.test.mjs"
|
|
21
21
|
},
|
|
22
22
|
"license": "Apache-2.0",
|
|
23
23
|
"author": "Trooth, LLC",
|
|
@@ -36,9 +36,7 @@
|
|
|
36
36
|
"vendor-lookup",
|
|
37
37
|
"due-diligence",
|
|
38
38
|
"procurement",
|
|
39
|
-
"trust-network",
|
|
40
39
|
"company-directory",
|
|
41
|
-
"mcp",
|
|
42
40
|
"ai-agents",
|
|
43
41
|
"agent-tools",
|
|
44
42
|
"signed-records",
|