@aldegad/safedeps 2.1.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/ARCHITECTURE.md +595 -0
- package/LICENSE +190 -0
- package/README.md +311 -0
- package/ROADMAP.md +131 -0
- package/SKILL.md +200 -0
- package/agents/openai.yaml +4 -0
- package/bin/safedeps +842 -0
- package/lib/ledger/ledger.sh +346 -0
- package/lib/providers/providers.sh +479 -0
- package/package.json +41 -0
- package/scripts/install/install-safedeps-hooks.mjs +209 -0
- package/scripts/install/install-safedeps-recheck-agent.mjs +203 -0
- package/scripts/install/migrate-safedeps-state.mjs +91 -0
- package/scripts/safedeps-post-verify.sh +584 -0
- package/scripts/safedeps-pre-guard.sh +427 -0
- package/scripts/safedeps-recheck-alert.sh +115 -0
- package/scripts/test/e2e.sh +107 -0
- package/scripts/test/fixture-provider.mjs +104 -0
- package/scripts/test/smoke.sh +89 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Safedeps approved spec ledger.
|
|
3
|
+
# Canonical owner for approved dependency specs under ~/.safedeps/approved-specs.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SAFEDEPS_HOME="${SAFEDEPS_HOME:-${HOME}/.safedeps}"
|
|
8
|
+
SAFEDEPS_LEDGER_DIR="${SAFEDEPS_LEDGER_DIR:-${SAFEDEPS_HOME}/approved-specs}"
|
|
9
|
+
SAFEDEPS_LEDGER_DEFAULT_TTL_DAYS="${SAFEDEPS_LEDGER_DEFAULT_TTL_DAYS:-30}"
|
|
10
|
+
|
|
11
|
+
safedeps_ledger_init() {
|
|
12
|
+
umask 077
|
|
13
|
+
mkdir -p "${SAFEDEPS_LEDGER_DIR}"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
safedeps_ledger_require_jq() {
|
|
17
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
18
|
+
printf 'safedeps ledger: jq is required\n' >&2
|
|
19
|
+
return 1
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
safedeps_ledger_now_iso() {
|
|
24
|
+
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
safedeps_ledger_add_days_iso() {
|
|
28
|
+
local days="$1"
|
|
29
|
+
local seconds
|
|
30
|
+
|
|
31
|
+
seconds=$(( days * 86400 ))
|
|
32
|
+
if date -u -r $(( $(date +%s) + seconds )) +"%Y-%m-%dT%H:%M:%SZ" >/dev/null 2>&1; then
|
|
33
|
+
date -u -r $(( $(date +%s) + seconds )) +"%Y-%m-%dT%H:%M:%SZ"
|
|
34
|
+
else
|
|
35
|
+
date -u -d "@$(( $(date +%s) + seconds ))" +"%Y-%m-%dT%H:%M:%SZ"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
safedeps_ledger_epoch() {
|
|
40
|
+
local timestamp="$1"
|
|
41
|
+
|
|
42
|
+
if date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "${timestamp}" +%s >/dev/null 2>&1; then
|
|
43
|
+
date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "${timestamp}" +%s
|
|
44
|
+
else
|
|
45
|
+
date -u -d "${timestamp}" +%s
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
safedeps_ledger_sha256_hex() {
|
|
50
|
+
local input="$1"
|
|
51
|
+
|
|
52
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
53
|
+
printf '%s' "${input}" | shasum -a 256 | cut -d' ' -f1
|
|
54
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
55
|
+
printf '%s' "${input}" | sha256sum | cut -d' ' -f1
|
|
56
|
+
else
|
|
57
|
+
printf 'safedeps ledger: shasum or sha256sum is required\n' >&2
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
safedeps_ledger_hash() {
|
|
63
|
+
local ecosystem="$1"
|
|
64
|
+
local package_name="$2"
|
|
65
|
+
local version="$3"
|
|
66
|
+
local hex
|
|
67
|
+
|
|
68
|
+
hex=$(safedeps_ledger_sha256_hex "${ecosystem}
|
|
69
|
+
${package_name}
|
|
70
|
+
${version}")
|
|
71
|
+
printf 'sha256:%s' "${hex}"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
safedeps_ledger_hash_to_filename() {
|
|
75
|
+
local hash="$1"
|
|
76
|
+
|
|
77
|
+
printf '%s.json' "${hash/:/-}"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
safedeps_ledger_path_for_hash() {
|
|
81
|
+
local hash="$1"
|
|
82
|
+
|
|
83
|
+
safedeps_ledger_init
|
|
84
|
+
printf '%s/%s' "${SAFEDEPS_LEDGER_DIR}" "$(safedeps_ledger_hash_to_filename "${hash}")"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
safedeps_ledger_path() {
|
|
88
|
+
local ecosystem="$1"
|
|
89
|
+
local package_name="$2"
|
|
90
|
+
local version="$3"
|
|
91
|
+
local hash
|
|
92
|
+
|
|
93
|
+
hash=$(safedeps_ledger_hash "${ecosystem}" "${package_name}" "${version}")
|
|
94
|
+
safedeps_ledger_path_for_hash "${hash}"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
safedeps_ledger_validate_json() {
|
|
98
|
+
local ledger_file="$1"
|
|
99
|
+
|
|
100
|
+
safedeps_ledger_require_jq || return 1
|
|
101
|
+
jq -e '
|
|
102
|
+
type == "object"
|
|
103
|
+
and (.hash | type == "string" and startswith("sha256:"))
|
|
104
|
+
and (.ecosystem | type == "string" and length > 0)
|
|
105
|
+
and (.package | type == "string" and length > 0)
|
|
106
|
+
and (.version | type == "string" and length > 0)
|
|
107
|
+
and (.version_range | type == "string")
|
|
108
|
+
and (.approved_at | type == "string" and length > 0)
|
|
109
|
+
and (.expires_at | type == "string" and length > 0)
|
|
110
|
+
and (.approved_by | type == "string")
|
|
111
|
+
and (.evidence | type == "object")
|
|
112
|
+
and ((.transitive_specs // []) | type == "array")
|
|
113
|
+
' "${ledger_file}" >/dev/null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
safedeps_ledger_is_expired_file() {
|
|
117
|
+
local ledger_file="$1"
|
|
118
|
+
local expires_at
|
|
119
|
+
local expires_epoch
|
|
120
|
+
local now_epoch
|
|
121
|
+
|
|
122
|
+
[[ -f "${ledger_file}" ]] || return 0
|
|
123
|
+
expires_at=$(jq -r '.expires_at // empty' "${ledger_file}" 2>/dev/null || true)
|
|
124
|
+
[[ -n "${expires_at}" ]] || return 0
|
|
125
|
+
|
|
126
|
+
if ! expires_epoch=$(safedeps_ledger_epoch "${expires_at}" 2>/dev/null); then
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
now_epoch=$(date +%s)
|
|
131
|
+
[[ "${expires_epoch}" -le "${now_epoch}" ]]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
safedeps_ledger_read() {
|
|
135
|
+
local ecosystem="$1"
|
|
136
|
+
local package_name="$2"
|
|
137
|
+
local version="$3"
|
|
138
|
+
local ledger_file
|
|
139
|
+
|
|
140
|
+
ledger_file=$(safedeps_ledger_path "${ecosystem}" "${package_name}" "${version}")
|
|
141
|
+
[[ -f "${ledger_file}" ]] || return 1
|
|
142
|
+
safedeps_ledger_validate_json "${ledger_file}" || return 1
|
|
143
|
+
cat "${ledger_file}"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
safedeps_ledger_check() {
|
|
147
|
+
local ecosystem="$1"
|
|
148
|
+
local package_name="$2"
|
|
149
|
+
local version="$3"
|
|
150
|
+
local ledger_file
|
|
151
|
+
local expected_hash
|
|
152
|
+
local stored_hash
|
|
153
|
+
|
|
154
|
+
ledger_file=$(safedeps_ledger_path "${ecosystem}" "${package_name}" "${version}")
|
|
155
|
+
expected_hash=$(safedeps_ledger_hash "${ecosystem}" "${package_name}" "${version}")
|
|
156
|
+
|
|
157
|
+
if [[ ! -f "${ledger_file}" ]]; then
|
|
158
|
+
jq -cn --arg hash "${expected_hash}" '{approved: false, reason: "miss", hash: $hash}'
|
|
159
|
+
return 1
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
if ! safedeps_ledger_validate_json "${ledger_file}"; then
|
|
163
|
+
jq -cn --arg hash "${expected_hash}" '{approved: false, reason: "invalid", hash: $hash}'
|
|
164
|
+
return 1
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
stored_hash=$(jq -r '.hash' "${ledger_file}")
|
|
168
|
+
if [[ "${stored_hash}" != "${expected_hash}" ]]; then
|
|
169
|
+
jq -cn --arg hash "${expected_hash}" --arg stored_hash "${stored_hash}" \
|
|
170
|
+
'{approved: false, reason: "hash_mismatch", hash: $hash, stored_hash: $stored_hash}'
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
if safedeps_ledger_is_expired_file "${ledger_file}"; then
|
|
175
|
+
jq -cn --arg hash "${expected_hash}" --slurpfile spec "${ledger_file}" \
|
|
176
|
+
'{approved: false, reason: "expired", hash: $hash, spec: $spec[0]}'
|
|
177
|
+
return 1
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
jq -cn --arg hash "${expected_hash}" --slurpfile spec "${ledger_file}" \
|
|
181
|
+
'{approved: true, reason: "hit", hash: $hash, spec: $spec[0]}'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
safedeps_ledger_atomic_write() {
|
|
185
|
+
local target_path="$1"
|
|
186
|
+
local temp_path="${target_path}.$$"
|
|
187
|
+
|
|
188
|
+
safedeps_ledger_init
|
|
189
|
+
cat > "${temp_path}"
|
|
190
|
+
chmod 600 "${temp_path}" 2>/dev/null || true
|
|
191
|
+
safedeps_ledger_validate_json "${temp_path}" || {
|
|
192
|
+
rm -f "${temp_path}"
|
|
193
|
+
return 1
|
|
194
|
+
}
|
|
195
|
+
mv "${temp_path}" "${target_path}"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
safedeps_ledger_write_approved_spec() {
|
|
199
|
+
local ecosystem="$1"
|
|
200
|
+
local package_name="$2"
|
|
201
|
+
local version="$3"
|
|
202
|
+
local version_range="${4:-$3}"
|
|
203
|
+
local approved_by="${5:-local}"
|
|
204
|
+
local evidence_file="${6:-}"
|
|
205
|
+
local ttl_days="${7:-${SAFEDEPS_LEDGER_DEFAULT_TTL_DAYS}}"
|
|
206
|
+
local approved_at
|
|
207
|
+
local expires_at
|
|
208
|
+
local hash
|
|
209
|
+
local target_path
|
|
210
|
+
local evidence_arg=()
|
|
211
|
+
|
|
212
|
+
safedeps_ledger_require_jq || return 1
|
|
213
|
+
safedeps_ledger_init
|
|
214
|
+
|
|
215
|
+
approved_at=$(safedeps_ledger_now_iso)
|
|
216
|
+
expires_at=$(safedeps_ledger_add_days_iso "${ttl_days}")
|
|
217
|
+
hash=$(safedeps_ledger_hash "${ecosystem}" "${package_name}" "${version}")
|
|
218
|
+
target_path=$(safedeps_ledger_path_for_hash "${hash}")
|
|
219
|
+
|
|
220
|
+
if [[ -n "${evidence_file}" ]]; then
|
|
221
|
+
[[ -f "${evidence_file}" ]] || {
|
|
222
|
+
printf 'safedeps ledger: evidence file not found: %s\n' "${evidence_file}" >&2
|
|
223
|
+
return 1
|
|
224
|
+
}
|
|
225
|
+
evidence_arg=(--slurpfile evidence "${evidence_file}")
|
|
226
|
+
else
|
|
227
|
+
evidence_arg=(--argjson evidence '{}')
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
if [[ -n "${evidence_file}" ]]; then
|
|
231
|
+
jq -cn \
|
|
232
|
+
--arg hash "${hash}" \
|
|
233
|
+
--arg ecosystem "${ecosystem}" \
|
|
234
|
+
--arg package "${package_name}" \
|
|
235
|
+
--arg version "${version}" \
|
|
236
|
+
--arg version_range "${version_range}" \
|
|
237
|
+
--arg approved_at "${approved_at}" \
|
|
238
|
+
--arg expires_at "${expires_at}" \
|
|
239
|
+
--arg approved_by "${approved_by}" \
|
|
240
|
+
"${evidence_arg[@]}" \
|
|
241
|
+
'{
|
|
242
|
+
hash: $hash,
|
|
243
|
+
ecosystem: $ecosystem,
|
|
244
|
+
package: $package,
|
|
245
|
+
version: $version,
|
|
246
|
+
version_range: $version_range,
|
|
247
|
+
approved_at: $approved_at,
|
|
248
|
+
expires_at: $expires_at,
|
|
249
|
+
approved_by: $approved_by,
|
|
250
|
+
evidence: ($evidence[0] // {}),
|
|
251
|
+
transitive_specs: []
|
|
252
|
+
}' | safedeps_ledger_atomic_write "${target_path}"
|
|
253
|
+
else
|
|
254
|
+
jq -cn \
|
|
255
|
+
--arg hash "${hash}" \
|
|
256
|
+
--arg ecosystem "${ecosystem}" \
|
|
257
|
+
--arg package "${package_name}" \
|
|
258
|
+
--arg version "${version}" \
|
|
259
|
+
--arg version_range "${version_range}" \
|
|
260
|
+
--arg approved_at "${approved_at}" \
|
|
261
|
+
--arg expires_at "${expires_at}" \
|
|
262
|
+
--arg approved_by "${approved_by}" \
|
|
263
|
+
"${evidence_arg[@]}" \
|
|
264
|
+
'{
|
|
265
|
+
hash: $hash,
|
|
266
|
+
ecosystem: $ecosystem,
|
|
267
|
+
package: $package,
|
|
268
|
+
version: $version,
|
|
269
|
+
version_range: $version_range,
|
|
270
|
+
approved_at: $approved_at,
|
|
271
|
+
expires_at: $expires_at,
|
|
272
|
+
approved_by: $approved_by,
|
|
273
|
+
evidence: $evidence,
|
|
274
|
+
transitive_specs: []
|
|
275
|
+
}' | safedeps_ledger_atomic_write "${target_path}"
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
cat "${target_path}"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
safedeps_ledger_revoke() {
|
|
282
|
+
local ecosystem="$1"
|
|
283
|
+
local package_name="$2"
|
|
284
|
+
local version="$3"
|
|
285
|
+
local reason="${4:-revoked}"
|
|
286
|
+
local ledger_file
|
|
287
|
+
local temp_path
|
|
288
|
+
local revoked_at
|
|
289
|
+
|
|
290
|
+
ledger_file=$(safedeps_ledger_path "${ecosystem}" "${package_name}" "${version}")
|
|
291
|
+
[[ -f "${ledger_file}" ]] || return 1
|
|
292
|
+
safedeps_ledger_validate_json "${ledger_file}" || return 1
|
|
293
|
+
|
|
294
|
+
temp_path="${ledger_file}.$$"
|
|
295
|
+
revoked_at=$(safedeps_ledger_now_iso)
|
|
296
|
+
jq \
|
|
297
|
+
--arg revoked_at "${revoked_at}" \
|
|
298
|
+
--arg reason "${reason}" \
|
|
299
|
+
'. + {revoked_at: $revoked_at, revoked_reason: $reason, expires_at: $revoked_at}' \
|
|
300
|
+
"${ledger_file}" > "${temp_path}"
|
|
301
|
+
chmod 600 "${temp_path}" 2>/dev/null || true
|
|
302
|
+
safedeps_ledger_validate_json "${temp_path}" || {
|
|
303
|
+
rm -f "${temp_path}"
|
|
304
|
+
return 1
|
|
305
|
+
}
|
|
306
|
+
mv "${temp_path}" "${ledger_file}"
|
|
307
|
+
cat "${ledger_file}"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
311
|
+
command_name="${1:-}"
|
|
312
|
+
shift || true
|
|
313
|
+
|
|
314
|
+
case "${command_name}" in
|
|
315
|
+
hash)
|
|
316
|
+
[[ "$#" -eq 3 ]] || { printf 'usage: %s hash <ecosystem> <package> <version>\n' "$0" >&2; exit 2; }
|
|
317
|
+
safedeps_ledger_hash "$@"
|
|
318
|
+
;;
|
|
319
|
+
path)
|
|
320
|
+
[[ "$#" -eq 3 ]] || { printf 'usage: %s path <ecosystem> <package> <version>\n' "$0" >&2; exit 2; }
|
|
321
|
+
safedeps_ledger_path "$@"
|
|
322
|
+
;;
|
|
323
|
+
check)
|
|
324
|
+
[[ "$#" -eq 3 ]] || { printf 'usage: %s check <ecosystem> <package> <version>\n' "$0" >&2; exit 2; }
|
|
325
|
+
safedeps_ledger_check "$@"
|
|
326
|
+
;;
|
|
327
|
+
approve)
|
|
328
|
+
if [[ "$#" -lt 3 || "$#" -gt 7 ]]; then
|
|
329
|
+
printf 'usage: %s approve <ecosystem> <package> <version> [version_range] [approved_by] [evidence_file] [ttl_days]\n' "$0" >&2
|
|
330
|
+
exit 2
|
|
331
|
+
fi
|
|
332
|
+
safedeps_ledger_write_approved_spec "$@"
|
|
333
|
+
;;
|
|
334
|
+
revoke)
|
|
335
|
+
if [[ "$#" -lt 3 || "$#" -gt 4 ]]; then
|
|
336
|
+
printf 'usage: %s revoke <ecosystem> <package> <version> [reason]\n' "$0" >&2
|
|
337
|
+
exit 2
|
|
338
|
+
fi
|
|
339
|
+
safedeps_ledger_revoke "$@"
|
|
340
|
+
;;
|
|
341
|
+
*)
|
|
342
|
+
printf 'usage: %s {hash|path|check|approve|revoke} ...\n' "$0" >&2
|
|
343
|
+
exit 2
|
|
344
|
+
;;
|
|
345
|
+
esac
|
|
346
|
+
fi
|