@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.
@@ -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