@enactprotocol/cli 1.2.13 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/package.json +34 -38
- package/src/commands/auth/index.ts +940 -0
- package/src/commands/cache/index.ts +361 -0
- package/src/commands/config/README.md +239 -0
- package/src/commands/config/index.ts +164 -0
- package/src/commands/env/README.md +197 -0
- package/src/commands/env/index.ts +392 -0
- package/src/commands/exec/README.md +110 -0
- package/src/commands/exec/index.ts +195 -0
- package/src/commands/get/index.ts +198 -0
- package/src/commands/index.ts +30 -0
- package/src/commands/inspect/index.ts +264 -0
- package/src/commands/install/README.md +146 -0
- package/src/commands/install/index.ts +682 -0
- package/src/commands/list/README.md +115 -0
- package/src/commands/list/index.ts +138 -0
- package/src/commands/publish/index.ts +350 -0
- package/src/commands/report/index.ts +366 -0
- package/src/commands/run/README.md +124 -0
- package/src/commands/run/index.ts +686 -0
- package/src/commands/search/index.ts +368 -0
- package/src/commands/setup/index.ts +274 -0
- package/src/commands/sign/index.ts +652 -0
- package/src/commands/trust/README.md +214 -0
- package/src/commands/trust/index.ts +453 -0
- package/src/commands/unyank/index.ts +107 -0
- package/src/commands/yank/index.ts +143 -0
- package/src/index.ts +96 -0
- package/src/types.ts +81 -0
- package/src/utils/errors.ts +409 -0
- package/src/utils/exit-codes.ts +159 -0
- package/src/utils/ignore.ts +147 -0
- package/src/utils/index.ts +107 -0
- package/src/utils/output.ts +242 -0
- package/src/utils/spinner.ts +214 -0
- package/tests/commands/auth.test.ts +217 -0
- package/tests/commands/cache.test.ts +286 -0
- package/tests/commands/config.test.ts +277 -0
- package/tests/commands/env.test.ts +293 -0
- package/tests/commands/exec.test.ts +112 -0
- package/tests/commands/get.test.ts +179 -0
- package/tests/commands/inspect.test.ts +201 -0
- package/tests/commands/install-integration.test.ts +343 -0
- package/tests/commands/install.test.ts +288 -0
- package/tests/commands/list.test.ts +160 -0
- package/tests/commands/publish.test.ts +186 -0
- package/tests/commands/report.test.ts +194 -0
- package/tests/commands/run.test.ts +231 -0
- package/tests/commands/search.test.ts +131 -0
- package/tests/commands/sign.test.ts +164 -0
- package/tests/commands/trust.test.ts +236 -0
- package/tests/commands/unyank.test.ts +114 -0
- package/tests/commands/yank.test.ts +154 -0
- package/tests/e2e.test.ts +554 -0
- package/tests/fixtures/calculator/enact.yaml +34 -0
- package/tests/fixtures/echo-tool/enact.md +31 -0
- package/tests/fixtures/env-tool/enact.yaml +19 -0
- package/tests/fixtures/greeter/enact.yaml +18 -0
- package/tests/fixtures/invalid-tool/enact.yaml +4 -0
- package/tests/index.test.ts +8 -0
- package/tests/types.test.ts +84 -0
- package/tests/utils/errors.test.ts +303 -0
- package/tests/utils/exit-codes.test.ts +189 -0
- package/tests/utils/ignore.test.ts +461 -0
- package/tests/utils/output.test.ts +126 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/index.js +0 -231612
- package/dist/index.js.bak +0 -231611
- package/dist/web/static/app.js +0 -663
- package/dist/web/static/index.html +0 -117
- package/dist/web/static/style.css +0 -291
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# enact trust
|
|
2
|
+
|
|
3
|
+
Manage trusted publishers and auditors.
|
|
4
|
+
|
|
5
|
+
## Synopsis
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
enact trust [identity] [options]
|
|
9
|
+
enact trust <subcommand> [options]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Description
|
|
13
|
+
|
|
14
|
+
The `trust` command manages your trust configuration for Enact tools. Trust determines which tools can run and under what conditions.
|
|
15
|
+
|
|
16
|
+
Enact supports two types of trusted identities:
|
|
17
|
+
|
|
18
|
+
1. **Publishers** - Tool authors (e.g., `alice`, `EnactProtocol`)
|
|
19
|
+
2. **Auditors** - Third-party reviewers (e.g., `github:securityteam`)
|
|
20
|
+
|
|
21
|
+
## Quick Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Add a trusted publisher
|
|
25
|
+
enact trust alice
|
|
26
|
+
|
|
27
|
+
# Remove a trusted publisher
|
|
28
|
+
enact trust -r alice
|
|
29
|
+
|
|
30
|
+
# Add a trusted auditor (provider:identity format)
|
|
31
|
+
enact trust github:securityteam
|
|
32
|
+
|
|
33
|
+
# List all trusted identities
|
|
34
|
+
enact trust
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Identity Formats
|
|
38
|
+
|
|
39
|
+
| Type | Format | Examples |
|
|
40
|
+
|------|--------|----------|
|
|
41
|
+
| Publisher | `name` | `alice`, `EnactProtocol`, `mycompany` |
|
|
42
|
+
| Auditor | `provider:identity` | `github:user`, `google:user@example.com` |
|
|
43
|
+
|
|
44
|
+
The presence of a colon (`:`) determines whether an identity is treated as a publisher or auditor.
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
| Option | Description |
|
|
49
|
+
|--------|-------------|
|
|
50
|
+
| `-r, --remove` | Remove identity from trusted list |
|
|
51
|
+
| `--json` | Output as JSON |
|
|
52
|
+
|
|
53
|
+
## Subcommands
|
|
54
|
+
|
|
55
|
+
### trust (default)
|
|
56
|
+
|
|
57
|
+
With no subcommand and no identity, lists all trusted publishers and auditors.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
enact trust
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
With an identity, adds it to the trust list (or removes with `-r`).
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
enact trust alice # Add publisher
|
|
67
|
+
enact trust -r alice # Remove publisher
|
|
68
|
+
enact trust github:user # Add auditor
|
|
69
|
+
enact trust -r github:user # Remove auditor
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### trust list
|
|
73
|
+
|
|
74
|
+
List all trusted publishers and auditors.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
enact trust list [options]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Options:**
|
|
81
|
+
| Option | Description |
|
|
82
|
+
|--------|-------------|
|
|
83
|
+
| `--json` | Output as JSON |
|
|
84
|
+
|
|
85
|
+
**Example:**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
$ enact trust list
|
|
89
|
+
|
|
90
|
+
Trusted Publishers
|
|
91
|
+
• alice
|
|
92
|
+
• EnactProtocol
|
|
93
|
+
|
|
94
|
+
Trusted Auditors
|
|
95
|
+
• github:securityteam
|
|
96
|
+
|
|
97
|
+
Policy: warn
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### trust check
|
|
101
|
+
|
|
102
|
+
Check the trust status of a specific tool.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
enact trust check <tool> [options]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Arguments:**
|
|
109
|
+
| Argument | Description |
|
|
110
|
+
|----------|-------------|
|
|
111
|
+
| `tool` | Tool to check (name@version format) |
|
|
112
|
+
|
|
113
|
+
**Options:**
|
|
114
|
+
| Option | Description |
|
|
115
|
+
|--------|-------------|
|
|
116
|
+
| `--json` | Output as JSON |
|
|
117
|
+
|
|
118
|
+
**Example:**
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
enact trust check alice/api/slack-notifier@1.0.0
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> **Note:** Full trust verification requires the Trust Package (Phase 6).
|
|
125
|
+
|
|
126
|
+
## Trust Policy
|
|
127
|
+
|
|
128
|
+
The trust policy determines behavior when running untrusted tools:
|
|
129
|
+
|
|
130
|
+
| Policy | Behavior |
|
|
131
|
+
|--------|----------|
|
|
132
|
+
| `strict` | Refuse to run untrusted tools |
|
|
133
|
+
| `warn` | Warn but allow (default) |
|
|
134
|
+
| `allow` | Run without warnings |
|
|
135
|
+
|
|
136
|
+
Set the policy using the config command:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
enact config set trust.policy strict
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## How Trust Works
|
|
143
|
+
|
|
144
|
+
When you run a tool, Enact checks:
|
|
145
|
+
|
|
146
|
+
1. **Is the publisher trusted?**
|
|
147
|
+
- Tool signed by `alice` → check if `alice` is in `trust.publishers`
|
|
148
|
+
|
|
149
|
+
2. **Is an auditor trusted?**
|
|
150
|
+
- Tool audited by `github:user` → check if `github:user` is in `trust.auditors`
|
|
151
|
+
|
|
152
|
+
3. **Apply policy**
|
|
153
|
+
- If trusted: run normally
|
|
154
|
+
- If not trusted: apply policy (strict/warn/allow)
|
|
155
|
+
|
|
156
|
+
## Examples
|
|
157
|
+
|
|
158
|
+
### Set Up Trust for a Team
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Trust your company's namespace
|
|
162
|
+
enact trust mycompany
|
|
163
|
+
|
|
164
|
+
# Trust the official Enact tools
|
|
165
|
+
enact trust EnactProtocol
|
|
166
|
+
|
|
167
|
+
# Trust your security team as auditors
|
|
168
|
+
enact trust github:myorg/security-team
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Audit Current Trust
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# List everything
|
|
175
|
+
enact trust list
|
|
176
|
+
|
|
177
|
+
# Get as JSON for scripting
|
|
178
|
+
enact trust list --json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Check a Specific Tool
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Before running, check if tool is trusted
|
|
185
|
+
enact trust check alice/api/slack-notifier@1.0.0
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Configuration File
|
|
189
|
+
|
|
190
|
+
Trust settings are stored in `~/.enact/config.yaml`:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
trust:
|
|
194
|
+
publishers:
|
|
195
|
+
- alice
|
|
196
|
+
- EnactProtocol
|
|
197
|
+
auditors:
|
|
198
|
+
- github:securityteam
|
|
199
|
+
policy: warn
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
You can edit this file directly or use the `enact trust` and `enact config` commands.
|
|
203
|
+
|
|
204
|
+
## Exit Codes
|
|
205
|
+
|
|
206
|
+
| Code | Description |
|
|
207
|
+
|------|-------------|
|
|
208
|
+
| `0` | Success |
|
|
209
|
+
| `1` | Error |
|
|
210
|
+
|
|
211
|
+
## See Also
|
|
212
|
+
|
|
213
|
+
- [enact config](../config/README.md) - Manage CLI configuration including trust policy
|
|
214
|
+
- [enact run](../run/README.md) - How trust affects tool execution
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enact trust command
|
|
3
|
+
*
|
|
4
|
+
* Manage trusted identities for attestation verification.
|
|
5
|
+
* Uses a unified model: all trust is based on cryptographic attestations.
|
|
6
|
+
* Publishers who want their tools trusted should self-sign them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type AttestationListResponse,
|
|
11
|
+
addTrustedAuditor as addTrustedAuditorToRegistry,
|
|
12
|
+
createApiClient,
|
|
13
|
+
getAttestationList,
|
|
14
|
+
getMyTrustedAuditors,
|
|
15
|
+
getToolVersion,
|
|
16
|
+
removeTrustedAuditor as removeTrustedAuditorFromRegistry,
|
|
17
|
+
verifyAllAttestations,
|
|
18
|
+
} from "@enactprotocol/api";
|
|
19
|
+
import { getSecret } from "@enactprotocol/secrets";
|
|
20
|
+
import {
|
|
21
|
+
addTrustedIdentity,
|
|
22
|
+
getMinimumAttestations,
|
|
23
|
+
getTrustPolicy,
|
|
24
|
+
getTrustedIdentities,
|
|
25
|
+
removeTrustedIdentity,
|
|
26
|
+
} from "@enactprotocol/shared";
|
|
27
|
+
import type { Command } from "commander";
|
|
28
|
+
import type { CommandContext, GlobalOptions } from "../../types";
|
|
29
|
+
import {
|
|
30
|
+
dim,
|
|
31
|
+
error,
|
|
32
|
+
formatError,
|
|
33
|
+
header,
|
|
34
|
+
info,
|
|
35
|
+
json,
|
|
36
|
+
keyValue,
|
|
37
|
+
listItem,
|
|
38
|
+
newline,
|
|
39
|
+
success,
|
|
40
|
+
warning,
|
|
41
|
+
} from "../../utils";
|
|
42
|
+
|
|
43
|
+
/** Auth namespace for token storage */
|
|
44
|
+
const AUTH_NAMESPACE = "enact:auth";
|
|
45
|
+
const ACCESS_TOKEN_KEY = "access_token";
|
|
46
|
+
|
|
47
|
+
interface TrustOptions extends GlobalOptions {
|
|
48
|
+
remove?: boolean;
|
|
49
|
+
sync?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface TrustCheckOptions extends GlobalOptions {}
|
|
53
|
+
|
|
54
|
+
interface TrustListOptions extends GlobalOptions {
|
|
55
|
+
sync?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate identity format
|
|
60
|
+
* Must be provider:identity format (e.g., github:alice, google:user@example.com)
|
|
61
|
+
*/
|
|
62
|
+
function validateIdentity(identity: string): { valid: boolean; error?: string } {
|
|
63
|
+
if (!identity.includes(":")) {
|
|
64
|
+
return {
|
|
65
|
+
valid: false,
|
|
66
|
+
error:
|
|
67
|
+
"Invalid identity format. Use provider:identity format (e.g., github:alice, google:user@example.com)",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [provider, ...rest] = identity.split(":");
|
|
72
|
+
const id = rest.join(":"); // Handle cases like google:user@example.com
|
|
73
|
+
|
|
74
|
+
if (!provider || !id) {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: "Invalid identity format. Use provider:identity format (e.g., github:alice)",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const validProviders = ["github", "google", "microsoft", "gitlab"];
|
|
82
|
+
if (!validProviders.includes(provider) && !provider.startsWith("http")) {
|
|
83
|
+
warning(`Unknown provider '${provider}'. Common providers: ${validProviders.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { valid: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Add a trusted identity
|
|
91
|
+
*/
|
|
92
|
+
async function addTrust(
|
|
93
|
+
identity: string,
|
|
94
|
+
options: TrustOptions,
|
|
95
|
+
_ctx: CommandContext
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
// Validate identity format
|
|
98
|
+
const validation = validateIdentity(identity);
|
|
99
|
+
if (!validation.valid) {
|
|
100
|
+
error(validation.error!);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add to local config
|
|
105
|
+
const added = addTrustedIdentity(identity);
|
|
106
|
+
if (added) {
|
|
107
|
+
success(`Added ${identity} to trusted identities`);
|
|
108
|
+
} else {
|
|
109
|
+
info(`${identity} is already trusted`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sync to registry if authenticated and --sync flag
|
|
113
|
+
if (options.sync) {
|
|
114
|
+
const authToken = await getSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
|
|
115
|
+
if (authToken) {
|
|
116
|
+
const client = createApiClient();
|
|
117
|
+
client.setAuthToken(authToken);
|
|
118
|
+
try {
|
|
119
|
+
await addTrustedAuditorToRegistry(client, identity);
|
|
120
|
+
success(`Synced ${identity} to registry`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
warning(`Failed to sync to registry: ${formatError(err)}`);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
dim("Not authenticated - skipping registry sync");
|
|
126
|
+
dim("Run 'enact auth login' to enable registry sync");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.json) {
|
|
131
|
+
json({ added: true, identity });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove a trusted identity
|
|
137
|
+
*/
|
|
138
|
+
async function removeTrust(
|
|
139
|
+
identity: string,
|
|
140
|
+
options: TrustOptions,
|
|
141
|
+
_ctx: CommandContext
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
// Remove from local config
|
|
144
|
+
const removed = removeTrustedIdentity(identity);
|
|
145
|
+
if (removed) {
|
|
146
|
+
success(`Removed ${identity} from trusted identities`);
|
|
147
|
+
} else {
|
|
148
|
+
info(`${identity} was not in trusted list`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Sync to registry if authenticated and --sync flag
|
|
152
|
+
if (options.sync) {
|
|
153
|
+
const authToken = await getSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
|
|
154
|
+
if (authToken) {
|
|
155
|
+
const client = createApiClient();
|
|
156
|
+
client.setAuthToken(authToken);
|
|
157
|
+
try {
|
|
158
|
+
await removeTrustedAuditorFromRegistry(client, identity);
|
|
159
|
+
success(`Removed ${identity} from registry`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
warning(`Failed to sync to registry: ${formatError(err)}`);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
dim("Not authenticated - skipping registry sync");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (options.json) {
|
|
169
|
+
json({ removed: true, identity });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Trust command handler (add or remove)
|
|
175
|
+
*/
|
|
176
|
+
async function trustHandler(
|
|
177
|
+
identity: string,
|
|
178
|
+
options: TrustOptions,
|
|
179
|
+
ctx: CommandContext
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
if (options.remove) {
|
|
182
|
+
return removeTrust(identity, options, ctx);
|
|
183
|
+
}
|
|
184
|
+
return addTrust(identity, options, ctx);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* List trusted identities
|
|
189
|
+
*/
|
|
190
|
+
async function trustListHandler(options: TrustListOptions, _ctx: CommandContext): Promise<void> {
|
|
191
|
+
const auditors = getTrustedIdentities();
|
|
192
|
+
const policy = getTrustPolicy();
|
|
193
|
+
const minimumAttestations = getMinimumAttestations();
|
|
194
|
+
|
|
195
|
+
// Get remote identities if authenticated and --sync flag
|
|
196
|
+
let remoteIdentities: string[] = [];
|
|
197
|
+
if (options.sync) {
|
|
198
|
+
const authToken = await getSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
|
|
199
|
+
if (authToken) {
|
|
200
|
+
const client = createApiClient();
|
|
201
|
+
client.setAuthToken(authToken);
|
|
202
|
+
try {
|
|
203
|
+
remoteIdentities = await getMyTrustedAuditors(client);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
warning(`Failed to fetch remote trust config: ${formatError(err)}`);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
dim("Not authenticated - showing local config only");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (options.json) {
|
|
213
|
+
json({
|
|
214
|
+
identities: auditors,
|
|
215
|
+
remoteIdentities: options.sync ? remoteIdentities : undefined,
|
|
216
|
+
policy,
|
|
217
|
+
minimum_attestations: minimumAttestations,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
header("Trusted Identities (Local)");
|
|
223
|
+
newline();
|
|
224
|
+
if (auditors.length === 0) {
|
|
225
|
+
dim(" No trusted identities configured");
|
|
226
|
+
dim(" Add with: enact trust provider:identity");
|
|
227
|
+
} else {
|
|
228
|
+
for (const identity of auditors) {
|
|
229
|
+
listItem(identity, 2);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (options.sync && remoteIdentities.length > 0) {
|
|
234
|
+
newline();
|
|
235
|
+
header("Trusted Identities (Registry)");
|
|
236
|
+
newline();
|
|
237
|
+
for (const identity of remoteIdentities) {
|
|
238
|
+
listItem(identity, 2);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
newline();
|
|
243
|
+
dim(`Policy: ${policy}`);
|
|
244
|
+
dim(`Minimum attestations: ${minimumAttestations}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Parse tool@version syntax
|
|
249
|
+
*/
|
|
250
|
+
function parseToolSpec(spec: string): { name: string; version: string | undefined } {
|
|
251
|
+
const atIndex = spec.lastIndexOf("@");
|
|
252
|
+
if (atIndex === -1 || atIndex === 0) {
|
|
253
|
+
return { name: spec, version: undefined };
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
name: spec.slice(0, atIndex),
|
|
257
|
+
version: spec.slice(atIndex + 1),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check trust status of a tool
|
|
263
|
+
*/
|
|
264
|
+
async function trustCheckHandler(
|
|
265
|
+
tool: string,
|
|
266
|
+
options: TrustCheckOptions,
|
|
267
|
+
_ctx: CommandContext
|
|
268
|
+
): Promise<void> {
|
|
269
|
+
const { name, version } = parseToolSpec(tool);
|
|
270
|
+
|
|
271
|
+
if (!version) {
|
|
272
|
+
error("Please specify a version: tool-name@version");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const trustedIdentities = getTrustedIdentities();
|
|
277
|
+
const client = createApiClient();
|
|
278
|
+
|
|
279
|
+
const trustedBy: string[] = [];
|
|
280
|
+
let verifiedAuditors: string[] = [];
|
|
281
|
+
let allAttestors: string[] = [];
|
|
282
|
+
let bundleHash = "";
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
// Fetch tool version info to get bundle hash
|
|
286
|
+
const toolVersion = await getToolVersion(client, name, version);
|
|
287
|
+
bundleHash = toolVersion.bundle?.hash ?? "";
|
|
288
|
+
|
|
289
|
+
if (!bundleHash) {
|
|
290
|
+
warning("Cannot verify attestations: tool bundle hash not found");
|
|
291
|
+
} else {
|
|
292
|
+
// Fetch attestations from registry
|
|
293
|
+
const attestationsResponse: AttestationListResponse = await getAttestationList(
|
|
294
|
+
client,
|
|
295
|
+
name,
|
|
296
|
+
version
|
|
297
|
+
);
|
|
298
|
+
const attestations = attestationsResponse.attestations;
|
|
299
|
+
|
|
300
|
+
// Collect all attestors
|
|
301
|
+
allAttestors = attestations.map((att: { auditor: string }) => att.auditor);
|
|
302
|
+
|
|
303
|
+
if (attestations.length > 0) {
|
|
304
|
+
// Verify all attestations locally (never trust registry's verification status)
|
|
305
|
+
const verifiedResults = await verifyAllAttestations(client, name, version, bundleHash);
|
|
306
|
+
|
|
307
|
+
// Update verifiedAuditors with the new format
|
|
308
|
+
verifiedAuditors = verifiedResults.map((a) => a.providerIdentity);
|
|
309
|
+
|
|
310
|
+
// Check which verified auditors are trusted
|
|
311
|
+
for (const result of verifiedResults) {
|
|
312
|
+
if (trustedIdentities.includes(result.providerIdentity)) {
|
|
313
|
+
trustedBy.push(result.providerIdentity);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (options.json) {
|
|
320
|
+
json({
|
|
321
|
+
tool: name,
|
|
322
|
+
version,
|
|
323
|
+
trusted: false,
|
|
324
|
+
error: formatError(err),
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
warning(`Failed to check attestations: ${formatError(err)}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const trusted = trustedBy.length > 0;
|
|
332
|
+
const hasAnyAttestation = allAttestors.length > 0;
|
|
333
|
+
const verifiedCount = verifiedAuditors.length;
|
|
334
|
+
|
|
335
|
+
if (options.json) {
|
|
336
|
+
json({
|
|
337
|
+
tool: name,
|
|
338
|
+
version,
|
|
339
|
+
trusted,
|
|
340
|
+
trustedBy,
|
|
341
|
+
verifiedAuditors,
|
|
342
|
+
totalAttestations: allAttestors.length,
|
|
343
|
+
verifiedAttestations: verifiedCount,
|
|
344
|
+
checkedIdentities: trustedIdentities.length,
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
header(`Trust Status: ${name}@${version}`);
|
|
350
|
+
newline();
|
|
351
|
+
|
|
352
|
+
if (trusted) {
|
|
353
|
+
success("✓ Trusted");
|
|
354
|
+
keyValue("Verified by trusted identity(ies)", trustedBy.join(", "));
|
|
355
|
+
} else if (hasAnyAttestation) {
|
|
356
|
+
warning("⚠ Not trusted by any configured identities");
|
|
357
|
+
if (verifiedCount > 0) {
|
|
358
|
+
keyValue("Verified attestations", verifiedAuditors.join(", "));
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
warning("⚠ No attestations found");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
newline();
|
|
365
|
+
dim(`Total attestations: ${allAttestors.length}`);
|
|
366
|
+
dim(`Cryptographically verified: ${verifiedCount}`);
|
|
367
|
+
dim(`Trusted identities configured: ${trustedIdentities.length}`);
|
|
368
|
+
|
|
369
|
+
if (!trusted && verifiedCount > 0) {
|
|
370
|
+
newline();
|
|
371
|
+
info("To trust this tool, add one of the verified identities:");
|
|
372
|
+
for (const identity of verifiedAuditors.slice(0, 3)) {
|
|
373
|
+
dim(` enact trust ${identity}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Configure the trust command
|
|
380
|
+
*/
|
|
381
|
+
export function configureTrustCommand(program: Command): void {
|
|
382
|
+
const trust = program
|
|
383
|
+
.command("trust")
|
|
384
|
+
.description("Manage trusted publishers and auditors")
|
|
385
|
+
.argument("[identity]", "Identity to trust (format: provider:identity, e.g., github:alice)")
|
|
386
|
+
.option("-r, --remove", "Remove from trusted list instead of adding")
|
|
387
|
+
.option("-s, --sync", "Sync with registry (requires authentication)")
|
|
388
|
+
.option("--json", "Output as JSON")
|
|
389
|
+
.action(async (identity: string | undefined, options: TrustOptions) => {
|
|
390
|
+
const ctx: CommandContext = {
|
|
391
|
+
cwd: process.cwd(),
|
|
392
|
+
options,
|
|
393
|
+
isCI: Boolean(process.env.CI),
|
|
394
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
if (!identity) {
|
|
399
|
+
// No identity provided, show list
|
|
400
|
+
await trustListHandler(options, ctx);
|
|
401
|
+
} else {
|
|
402
|
+
await trustHandler(identity, options, ctx);
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
error(formatError(err));
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// trust list
|
|
411
|
+
trust
|
|
412
|
+
.command("list")
|
|
413
|
+
.description("List all trusted identities")
|
|
414
|
+
.option("-s, --sync", "Also show registry trust config (requires authentication)")
|
|
415
|
+
.option("--json", "Output as JSON")
|
|
416
|
+
.action(async (options: TrustListOptions) => {
|
|
417
|
+
const ctx: CommandContext = {
|
|
418
|
+
cwd: process.cwd(),
|
|
419
|
+
options,
|
|
420
|
+
isCI: Boolean(process.env.CI),
|
|
421
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
await trustListHandler(options, ctx);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
error(formatError(err));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// trust check
|
|
433
|
+
trust
|
|
434
|
+
.command("check")
|
|
435
|
+
.description("Check trust status of a tool")
|
|
436
|
+
.argument("<tool>", "Tool to check (name@version)")
|
|
437
|
+
.option("--json", "Output as JSON")
|
|
438
|
+
.action(async (tool: string, options: TrustCheckOptions) => {
|
|
439
|
+
const ctx: CommandContext = {
|
|
440
|
+
cwd: process.cwd(),
|
|
441
|
+
options,
|
|
442
|
+
isCI: Boolean(process.env.CI),
|
|
443
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
await trustCheckHandler(tool, options, ctx);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
error(formatError(err));
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|