trooth 0.4.1 → 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/LICENSE +190 -0
- package/README.md +131 -91
- package/bin/trooth.mjs +253 -62
- package/package.json +11 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may accept and charge a
|
|
167
|
+
fee for, acceptance of support, warranty, indemnity, or other
|
|
168
|
+
liability obligations and/or rights consistent with this License.
|
|
169
|
+
However, in accepting such obligations, You may act only on Your
|
|
170
|
+
own behalf and on Your sole responsibility, not on behalf of any
|
|
171
|
+
other Contributor, and only if You agree to indemnify, defend,
|
|
172
|
+
and hold each Contributor harmless for any liability incurred by,
|
|
173
|
+
or claims asserted against, such Contributor by reason of your
|
|
174
|
+
accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2025-2026 Trooth, LLC
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
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,65 +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
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## In GitHub Actions
|
|
176
|
+
|
|
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
|
|
155
183
|
```
|
|
156
184
|
|
|
157
|
-
|
|
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. |
|
|
158
193
|
|
|
159
|
-
The
|
|
160
|
-
|
|
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
|
-
|
|
204
|
+
Four read-only tools, public data, no key. The pattern is written up at [trooth.co/docs/agents](https://trooth.co/docs/agents).
|
|
167
205
|
|
|
168
|
-
|
|
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
|
|
206
|
+
## Removed in 0.4.0
|
|
179
207
|
|
|
180
208
|
Breaking, and deliberately so.
|
|
181
209
|
|
|
182
|
-
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
217
|
+
|
|
218
|
+
## Security
|
|
219
|
+
|
|
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)
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
Apache License 2.0. See [LICENSE](LICENSE).
|
|
234
|
+
|
|
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
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Trooth CLI. Command name: `trooth`
|
|
3
|
-
// Copyright
|
|
3
|
+
// Copyright 2025-2026 Trooth, LLC.
|
|
4
|
+
// Licensed under the Apache License, Version 2.0. See the LICENSE file in this
|
|
5
|
+
// repository, or http://www.apache.org/licenses/LICENSE-2.0
|
|
4
6
|
//
|
|
5
|
-
// The Trooth Network is one public record per company: identity, products and
|
|
6
|
-
// domain and marketing links, people, documents, security and privacy posture,
|
|
7
|
-
// procurement terms, relationships and sub-processors
|
|
8
|
-
//
|
|
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.
|
|
9
12
|
//
|
|
10
13
|
// Commands:
|
|
11
14
|
// trooth check <domain> Read a company's witnessed record from the public Trooth
|
|
@@ -16,9 +19,9 @@
|
|
|
16
19
|
// trooth --help Show help. trooth --version Show version.
|
|
17
20
|
//
|
|
18
21
|
// WHAT THIS TOOL DOES NOT DO, ON PURPOSE:
|
|
19
|
-
// It does not
|
|
20
|
-
// It does not check anything against a named
|
|
21
|
-
// 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.
|
|
22
25
|
// It publishes facts and counts, reported apart, and never adds them into one number.
|
|
23
26
|
//
|
|
24
27
|
// Exit codes (stable, for scripts):
|
|
@@ -37,12 +40,12 @@ import { join, relative, extname, basename } from 'node:path';
|
|
|
37
40
|
|
|
38
41
|
const API = process.env.TROOTH_API || 'https://api.trooth.co';
|
|
39
42
|
const require = createRequire(import.meta.url);
|
|
40
|
-
let VERSION = '0.4.
|
|
43
|
+
let VERSION = '0.4.3';
|
|
41
44
|
try { VERSION = require('../package.json').version; } catch {}
|
|
42
45
|
|
|
43
46
|
const EXIT = { OK: 0, FINDING: 1, USAGE: 2, UPSTREAM: 3 };
|
|
44
47
|
|
|
45
|
-
//
|
|
48
|
+
// Color only when stdout is a TTY and NO_COLOR is unset, so piped output is clean.
|
|
46
49
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
47
50
|
const c = (code) => (useColor ? code : '');
|
|
48
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');
|
|
@@ -92,7 +95,7 @@ const FLAGS = {
|
|
|
92
95
|
* means an old README, an old CI job or an old blog post gets a sentence that says
|
|
93
96
|
* what happened, instead of "unknown command". */
|
|
94
97
|
const RETIRED = {
|
|
95
|
-
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.',
|
|
96
99
|
eu: 'Trooth does not ingest regulation-specific evidence. Publish evidence on your company record at https://trooth.co/dashboard.',
|
|
97
100
|
preflight: 'Retired. `trooth lint` reads declared infrastructure facts locally instead.',
|
|
98
101
|
};
|
|
@@ -140,7 +143,7 @@ ${B}Examples${X}
|
|
|
140
143
|
trooth check stripe.com ${D}# read a company's witnessed record${X}
|
|
141
144
|
trooth check trooth.co --json ${D}# one JSON document on stdout, for scripting${X}
|
|
142
145
|
trooth lint ./infra ${D}# read declared facts + print a canonical digest${X}
|
|
143
|
-
trooth lint --json
|
|
146
|
+
trooth lint --json > trooth-lint.json ${D}# the same facts as one JSON document, for a CI artifact${X}
|
|
144
147
|
|
|
145
148
|
${B}Flags${X}
|
|
146
149
|
--json machine-readable JSON on stdout; diagnostics on stderr
|
|
@@ -151,7 +154,7 @@ ${B}Exit codes${X}
|
|
|
151
154
|
${D}check reads only public, already-published records. No key, no account.
|
|
152
155
|
lint is entirely local: it opens files, and opens no sockets. Your source never leaves.
|
|
153
156
|
Trooth publishes facts and counts, never one number that sums a company up.
|
|
154
|
-
Trooth
|
|
157
|
+
Trooth signs what it witnessed. It never signs on a company's behalf.${X}
|
|
155
158
|
`;
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -200,6 +203,37 @@ function count(obj) {
|
|
|
200
203
|
return { passed: obj.passed, total: obj.total };
|
|
201
204
|
}
|
|
202
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
|
+
|
|
203
237
|
function projectRecord(v, domain) {
|
|
204
238
|
const events = Array.isArray(v.events)
|
|
205
239
|
? v.events.filter((e) => e && e.type).map((e) => ({ type: String(e.type), at: e.at, detail: e.detail }))
|
|
@@ -238,11 +272,12 @@ async function check() {
|
|
|
238
272
|
if (asJson) {
|
|
239
273
|
emitJson({ domain, listed: false, record_url: `https://trooth.co/network/${encodeURIComponent(domain)}` });
|
|
240
274
|
} else {
|
|
241
|
-
out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed
|
|
242
|
-
out(`\n${D}
|
|
243
|
-
out(`
|
|
244
|
-
out(`
|
|
245
|
-
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`);
|
|
246
281
|
}
|
|
247
282
|
process.exit(EXIT.FINDING);
|
|
248
283
|
}
|
|
@@ -252,19 +287,22 @@ async function check() {
|
|
|
252
287
|
|
|
253
288
|
const when = fmtDate(rec.witnessed_at);
|
|
254
289
|
const since = fmtDate(rec.first_published_at);
|
|
255
|
-
out(`\n${J}${B}Trooth Network${X} ${D}//
|
|
290
|
+
out(`\n${J}${B}Trooth Network${X} ${D}// public · signed · read-only //${X}`);
|
|
256
291
|
out(`${B}${rec.company_name}${X} ${C}${domain}${X}`);
|
|
257
|
-
out(`
|
|
258
|
-
(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}`) +
|
|
259
294
|
(since ? ` ${D}first published ${since}${X}` : ''));
|
|
260
295
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
out(
|
|
267
|
-
|
|
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}`);
|
|
268
306
|
}
|
|
269
307
|
|
|
270
308
|
const ids = [];
|
|
@@ -272,14 +310,14 @@ async function check() {
|
|
|
272
310
|
if (rec.authority_key_id) ids.push(`${D}Key ${rec.authority_key_id}${X}`);
|
|
273
311
|
if (ids.length) out(ids.join(' '));
|
|
274
312
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
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}`);
|
|
280
318
|
}
|
|
281
319
|
|
|
282
|
-
out(`\n${D}A
|
|
320
|
+
out(`\n${D}A dated, point-in-time record. Trooth issues no verdict and no single number.`);
|
|
283
321
|
out(`Full record: ${X}${C}${rec.record_url}${X}${D} · Signing keys: ${rec.verify_keys}${X}\n`);
|
|
284
322
|
process.exit(EXIT.OK);
|
|
285
323
|
}
|
|
@@ -290,20 +328,31 @@ async function check() {
|
|
|
290
328
|
* It reads the infrastructure a repository DECLARES and reports those
|
|
291
329
|
* declarations as facts: how many storage resources declare encryption, which
|
|
292
330
|
* regions appear, how many rules declare exposure to the whole internet. It
|
|
293
|
-
* does not judge them. There is no verdict, no
|
|
294
|
-
*
|
|
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.
|
|
295
333
|
* Declaring public ingress is not a failing; a load balancer is supposed to be
|
|
296
|
-
* public. What the facts mean is the reader's
|
|
334
|
+
* public. What the facts mean is the reader's decision.
|
|
297
335
|
*
|
|
298
336
|
* 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
|
|
300
|
-
*
|
|
301
|
-
*
|
|
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
|
|
302
341
|
* publishing the tree it came from.
|
|
303
342
|
*
|
|
304
|
-
* It is a
|
|
305
|
-
*
|
|
306
|
-
*
|
|
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. */
|
|
307
356
|
|
|
308
357
|
const SKIP_DIRS = new Set([
|
|
309
358
|
'node_modules', '.git', '.terraform', '.next', 'dist', 'build', 'vendor',
|
|
@@ -355,6 +404,125 @@ const PUBLIC_RE = /\b(?:publicly_accessible\s*[:=]\s*true|acl\s*[:=]\s*["']publi
|
|
|
355
404
|
// A literal that looks like a credential sitting in the file. Reported as a
|
|
356
405
|
// count only: no file name, no line, and never the value itself.
|
|
357
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
|
+
}
|
|
358
526
|
|
|
359
527
|
function classify(file, text) {
|
|
360
528
|
const n = basename(file).toLowerCase();
|
|
@@ -391,7 +559,9 @@ function lint() {
|
|
|
391
559
|
const byKind = { terraform: 0, 'terraform-plan': 0, kubernetes: 0, container: 0 };
|
|
392
560
|
const regions = new Set();
|
|
393
561
|
const resourceTypes = new Map();
|
|
394
|
-
|
|
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;
|
|
395
565
|
let openIngress = 0, publicAccess = 0, inlineCredentials = 0;
|
|
396
566
|
|
|
397
567
|
for (const f of files) {
|
|
@@ -405,29 +575,44 @@ function lint() {
|
|
|
405
575
|
byKind[kind]++;
|
|
406
576
|
read++;
|
|
407
577
|
|
|
408
|
-
|
|
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++;
|
|
409
584
|
|
|
410
|
-
if (kind === 'terraform') {
|
|
585
|
+
if (kind === 'terraform' && !isJson) {
|
|
411
586
|
for (const m of text.matchAll(RESOURCE_RE)) {
|
|
412
587
|
resourceTypes.set(m[1], (resourceTypes.get(m[1]) || 0) + 1);
|
|
413
588
|
}
|
|
414
589
|
}
|
|
415
590
|
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
for (const
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
if (STORAGE_TYPE.test(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
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++;
|
|
427
605
|
}
|
|
428
|
-
for (const line of text.split('\n')) if (SECRET_RE.test(line)) inlineCredentials++;
|
|
429
606
|
}
|
|
430
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
|
+
|
|
431
616
|
if (read === 0) {
|
|
432
617
|
const msg = `no infrastructure declarations found under ${target}.`;
|
|
433
618
|
diag(`${A}nothing to read${X} ${msg}`);
|
|
@@ -464,7 +649,7 @@ function lint() {
|
|
|
464
649
|
root: (() => { const r = relative(process.cwd(), target); return !r ? '.' : r.startsWith('..') ? target : r; })(),
|
|
465
650
|
facts,
|
|
466
651
|
digest: `sha256:${digest}`,
|
|
467
|
-
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.',
|
|
468
653
|
};
|
|
469
654
|
|
|
470
655
|
if (asJson) { emitJson(doc); process.exit(EXIT.OK); }
|
|
@@ -493,10 +678,16 @@ function lint() {
|
|
|
493
678
|
|
|
494
679
|
out(`\n${B}Digest${X} ${C}${doc.digest}${X}`);
|
|
495
680
|
out(`${D}A SHA-256 over the facts above, in canonical form, with the timestamp excluded.`);
|
|
496
|
-
out(`The same tree
|
|
497
|
-
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}`);
|
|
498
689
|
|
|
499
|
-
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`);
|
|
500
691
|
out(`supposed to be public. Trooth issues no verdict here and checks nothing against`);
|
|
501
692
|
out(`any standard. Nothing left this machine: lint opens files and opens no sockets.`);
|
|
502
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,25 +1,31 @@
|
|
|
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"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
|
-
"README.md"
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=18"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
18
|
"check": "node ./bin/trooth.mjs check",
|
|
18
|
-
"lint": "node ./bin/trooth.mjs lint"
|
|
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 && node tests/action.test.mjs && node tests/lint.test.mjs"
|
|
19
21
|
},
|
|
20
|
-
"license": "
|
|
22
|
+
"license": "Apache-2.0",
|
|
21
23
|
"author": "Trooth, LLC",
|
|
22
24
|
"homepage": "https://trooth.co",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/troothllc/trooth-cli.git"
|
|
28
|
+
},
|
|
23
29
|
"bugs": {
|
|
24
30
|
"url": "https://trooth.co/contact"
|
|
25
31
|
},
|
|
@@ -30,9 +36,7 @@
|
|
|
30
36
|
"vendor-lookup",
|
|
31
37
|
"due-diligence",
|
|
32
38
|
"procurement",
|
|
33
|
-
"trust-network",
|
|
34
39
|
"company-directory",
|
|
35
|
-
"mcp",
|
|
36
40
|
"ai-agents",
|
|
37
41
|
"agent-tools",
|
|
38
42
|
"signed-records",
|