@aldegad/safedeps 2.1.1 → 2.4.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 +273 -463
- package/README.ko.md +76 -12
- package/README.md +107 -38
- package/ROADMAP.md +123 -84
- package/SECURITY.md +45 -0
- package/SKILL.md +86 -143
- package/bin/safedeps +419 -52
- package/lib/gates/audit.sh +36 -0
- package/lib/gates/doctor.sh +212 -0
- package/lib/gates/hooks.sh +131 -0
- package/lib/gates/repo-profile.sh +60 -0
- package/lib/gates/scan.sh +94 -0
- package/lib/gates/templates/gitleaks.private.toml.tmpl +45 -0
- package/lib/gates/templates/gitleaks.toml.tmpl +43 -0
- package/lib/gates/templates/pre-commit.tmpl +49 -0
- package/lib/ledger/ledger.sh +94 -16
- package/lib/npm/closure.sh +115 -0
- package/lib/providers/providers.sh +248 -26
- package/package.json +2 -1
- package/scripts/install/install-safedeps-hooks.mjs +65 -23
- package/scripts/release-gates.sh +252 -0
- package/scripts/safedeps-post-verify.sh +185 -15
- package/scripts/safedeps-pre-guard.sh +309 -39
- package/scripts/test/e2e.sh +228 -4
- package/scripts/test/fixture-provider.mjs +21 -0
- package/scripts/test/smoke.sh +212 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aldegad/safedeps",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Dependency install safety gate with OSV-backed advisory checks, approved-spec ledger enforcement, and reorg rollback hooks",
|
|
5
5
|
"main": "bin/safedeps",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"ARCHITECTURE.md",
|
|
17
17
|
"ROADMAP.md",
|
|
18
18
|
"SKILL.md",
|
|
19
|
+
"SECURITY.md",
|
|
19
20
|
"LICENSE"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
@@ -11,18 +11,20 @@
|
|
|
11
11
|
// node scripts/install/install-safedeps-hooks.mjs --uninstall
|
|
12
12
|
// node scripts/install/install-safedeps-hooks.mjs --link-bin (optional ~/.local/bin/safedeps)
|
|
13
13
|
|
|
14
|
-
import { existsSync, lstatSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, symlinkSync, unlinkSync, readlinkSync } from "node:fs";
|
|
14
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, symlinkSync, unlinkSync, readlinkSync, renameSync } from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
-
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
19
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const REPO_ROOT = resolve(HERE, "..", "..");
|
|
21
|
-
const HOME = homedir();
|
|
21
|
+
const HOME = process.env.HOME || homedir();
|
|
22
22
|
|
|
23
23
|
const SKILL_ID = "safedeps";
|
|
24
|
-
const
|
|
25
|
-
const
|
|
24
|
+
const PRE_HOOK_NAME = "safedeps-pre-guard.sh";
|
|
25
|
+
const POST_HOOK_NAME = "safedeps-post-verify.sh";
|
|
26
|
+
const REPO_PRE_HOOK = join(REPO_ROOT, "scripts", PRE_HOOK_NAME);
|
|
27
|
+
const REPO_POST_HOOK = join(REPO_ROOT, "scripts", POST_HOOK_NAME);
|
|
26
28
|
const CLI_BIN = join(REPO_ROOT, "bin", "safedeps");
|
|
27
29
|
|
|
28
30
|
const args = new Set(process.argv.slice(2));
|
|
@@ -71,7 +73,14 @@ function writeJsonWithBackup(path, value) {
|
|
|
71
73
|
} else {
|
|
72
74
|
mkdirSync(dirname(path), { recursive: true });
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
const tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
77
|
+
writeFileSync(tmpPath, JSON.stringify(value, null, 2) + "\n");
|
|
78
|
+
renameSync(tmpPath, path);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function engineHookCommand(engineRoot, hookName) {
|
|
82
|
+
const engineName = basename(engineRoot).replace(/^\./u, "");
|
|
83
|
+
return `~/.${engineName}/skills/${SKILL_ID}/scripts/${hookName}`;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
function ensureHook(config, eventName, command) {
|
|
@@ -79,6 +88,13 @@ function ensureHook(config, eventName, command) {
|
|
|
79
88
|
config.hooks[eventName] = config.hooks[eventName] ?? [];
|
|
80
89
|
const buckets = config.hooks[eventName];
|
|
81
90
|
|
|
91
|
+
const already = buckets.some((bucket) =>
|
|
92
|
+
bucket?.matcher === "Bash" &&
|
|
93
|
+
Array.isArray(bucket?.hooks) &&
|
|
94
|
+
bucket.hooks.some((h) => h && h.type === "command" && h.command === command),
|
|
95
|
+
);
|
|
96
|
+
if (already) return false;
|
|
97
|
+
|
|
82
98
|
let bashBucket = buckets.find((b) => b && b.matcher === "Bash");
|
|
83
99
|
if (!bashBucket) {
|
|
84
100
|
bashBucket = { matcher: "Bash", hooks: [] };
|
|
@@ -86,9 +102,6 @@ function ensureHook(config, eventName, command) {
|
|
|
86
102
|
}
|
|
87
103
|
bashBucket.hooks = bashBucket.hooks ?? [];
|
|
88
104
|
|
|
89
|
-
const already = bashBucket.hooks.some((h) => h && h.type === "command" && h.command === command);
|
|
90
|
-
if (already) return false;
|
|
91
|
-
|
|
92
105
|
bashBucket.hooks.push({ type: "command", command });
|
|
93
106
|
return true;
|
|
94
107
|
}
|
|
@@ -106,25 +119,49 @@ function removeHook(config, eventName, command) {
|
|
|
106
119
|
return changed;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
function
|
|
122
|
+
function isSafedepsHookCommand(command, hookName) {
|
|
123
|
+
if (typeof command !== "string") return false;
|
|
124
|
+
const normalized = command.replace(/\\/gu, "/");
|
|
125
|
+
if (normalized.includes("npm-reorg-guard")) return true;
|
|
126
|
+
return normalized.includes("/safedeps/") && normalized.endsWith(`/scripts/${hookName}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pruneNonCanonicalSafedepsHooks(config, eventName, canonicalCommand, hookName) {
|
|
110
130
|
const buckets = config?.hooks?.[eventName];
|
|
111
131
|
if (!Array.isArray(buckets)) return false;
|
|
112
132
|
let changed = false;
|
|
133
|
+
let seenCanonical = false;
|
|
113
134
|
for (const bucket of buckets) {
|
|
114
|
-
if (!bucket ||
|
|
135
|
+
if (!bucket || !Array.isArray(bucket.hooks)) continue;
|
|
115
136
|
const before = bucket.hooks.length;
|
|
116
137
|
bucket.hooks = bucket.hooks.filter((h) => {
|
|
117
138
|
const command = h?.command;
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
if (command === canonicalCommand) {
|
|
140
|
+
if (bucket.matcher !== "Bash") return false;
|
|
141
|
+
if (seenCanonical) return false;
|
|
142
|
+
seenCanonical = true;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return command === canonicalCommand || !isSafedepsHookCommand(command, hookName);
|
|
122
146
|
});
|
|
123
147
|
if (bucket.hooks.length !== before) changed = true;
|
|
124
148
|
}
|
|
125
149
|
return changed;
|
|
126
150
|
}
|
|
127
151
|
|
|
152
|
+
function pruneAllSafedepsHooks(config, eventName, hookName) {
|
|
153
|
+
const buckets = config?.hooks?.[eventName];
|
|
154
|
+
if (!Array.isArray(buckets)) return false;
|
|
155
|
+
let changed = false;
|
|
156
|
+
for (const bucket of buckets) {
|
|
157
|
+
if (!bucket || !Array.isArray(bucket.hooks)) continue;
|
|
158
|
+
const before = bucket.hooks.length;
|
|
159
|
+
bucket.hooks = bucket.hooks.filter((h) => !isSafedepsHookCommand(h?.command, hookName));
|
|
160
|
+
if (bucket.hooks.length !== before) changed = true;
|
|
161
|
+
}
|
|
162
|
+
return changed;
|
|
163
|
+
}
|
|
164
|
+
|
|
128
165
|
function installInEngine({ engineRoot, configPath, label }) {
|
|
129
166
|
if (!existsSync(engineRoot)) {
|
|
130
167
|
warn(`skip ${label} (${engineRoot} not present)`);
|
|
@@ -132,13 +169,15 @@ function installInEngine({ engineRoot, configPath, label }) {
|
|
|
132
169
|
}
|
|
133
170
|
const skillsRoot = join(engineRoot, "skills");
|
|
134
171
|
const skillLink = join(skillsRoot, SKILL_ID);
|
|
172
|
+
const preCommand = engineHookCommand(engineRoot, PRE_HOOK_NAME);
|
|
173
|
+
const postCommand = engineHookCommand(engineRoot, POST_HOOK_NAME);
|
|
135
174
|
|
|
136
175
|
if (UNINSTALL) {
|
|
137
176
|
removeSymlink(skillLink);
|
|
138
177
|
if (existsSync(configPath)) {
|
|
139
178
|
const cfg = readJson(configPath);
|
|
140
|
-
const pre = removeHook(cfg, "PreToolUse",
|
|
141
|
-
const post = removeHook(cfg, "PostToolUse",
|
|
179
|
+
const pre = removeHook(cfg, "PreToolUse", preCommand) || pruneAllSafedepsHooks(cfg, "PreToolUse", PRE_HOOK_NAME);
|
|
180
|
+
const post = removeHook(cfg, "PostToolUse", postCommand) || pruneAllSafedepsHooks(cfg, "PostToolUse", POST_HOOK_NAME);
|
|
142
181
|
if (pre || post) {
|
|
143
182
|
writeJsonWithBackup(configPath, cfg);
|
|
144
183
|
log(`patched ${configPath} (removed safedeps hooks)`);
|
|
@@ -152,10 +191,10 @@ function installInEngine({ engineRoot, configPath, label }) {
|
|
|
152
191
|
ensureSymlink(REPO_ROOT, skillLink);
|
|
153
192
|
|
|
154
193
|
const cfg = readJson(configPath);
|
|
155
|
-
const legacyPreRemoved =
|
|
156
|
-
const legacyPostRemoved =
|
|
157
|
-
const preAdded = ensureHook(cfg, "PreToolUse",
|
|
158
|
-
const postAdded = ensureHook(cfg, "PostToolUse",
|
|
194
|
+
const legacyPreRemoved = pruneNonCanonicalSafedepsHooks(cfg, "PreToolUse", preCommand, PRE_HOOK_NAME);
|
|
195
|
+
const legacyPostRemoved = pruneNonCanonicalSafedepsHooks(cfg, "PostToolUse", postCommand, POST_HOOK_NAME);
|
|
196
|
+
const preAdded = ensureHook(cfg, "PreToolUse", preCommand);
|
|
197
|
+
const postAdded = ensureHook(cfg, "PostToolUse", postCommand);
|
|
159
198
|
if (legacyPreRemoved || legacyPostRemoved || preAdded || postAdded) {
|
|
160
199
|
writeJsonWithBackup(configPath, cfg);
|
|
161
200
|
log(`patched ${configPath} (pre=${preAdded ? "added" : "ok"}, post=${postAdded ? "added" : "ok"}, legacy=${legacyPreRemoved || legacyPostRemoved ? "removed" : "ok"})`);
|
|
@@ -181,8 +220,8 @@ function unlinkBin() {
|
|
|
181
220
|
}
|
|
182
221
|
|
|
183
222
|
function main() {
|
|
184
|
-
if (!existsSync(
|
|
185
|
-
throw new Error(`hook scripts not found at ${
|
|
223
|
+
if (!existsSync(REPO_PRE_HOOK) || !existsSync(REPO_POST_HOOK)) {
|
|
224
|
+
throw new Error(`hook scripts not found at ${REPO_PRE_HOOK} / ${REPO_POST_HOOK}`);
|
|
186
225
|
}
|
|
187
226
|
|
|
188
227
|
installInEngine({
|
|
@@ -203,6 +242,9 @@ function main() {
|
|
|
203
242
|
log("uninstall done.");
|
|
204
243
|
} else {
|
|
205
244
|
log("install done. New hook events fire on the next session start.");
|
|
245
|
+
// The dependency-install gate is global. The secret-leak lane is per-repo
|
|
246
|
+
// and stays opt-in (its policy lives in each repo). Nudge, do not auto-write.
|
|
247
|
+
log("secret-leak lane is per-repo — in a repo run: safedeps doctor (then `safedeps doctor --fix` to scaffold + activate).");
|
|
206
248
|
}
|
|
207
249
|
}
|
|
208
250
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT=""
|
|
5
|
+
STRICT=0
|
|
6
|
+
NO_RUN=0
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
cat <<'EOF'
|
|
10
|
+
Usage: run-release-gates.sh [--root <repo>] [--strict] [--no-run]
|
|
11
|
+
|
|
12
|
+
Runs release-time security gates for the current repository tree.
|
|
13
|
+
EOF
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
while [ $# -gt 0 ]; do
|
|
17
|
+
case "$1" in
|
|
18
|
+
--root)
|
|
19
|
+
ROOT="${2:-}"
|
|
20
|
+
shift 2
|
|
21
|
+
;;
|
|
22
|
+
--strict)
|
|
23
|
+
STRICT=1
|
|
24
|
+
shift
|
|
25
|
+
;;
|
|
26
|
+
--no-run)
|
|
27
|
+
NO_RUN=1
|
|
28
|
+
shift
|
|
29
|
+
;;
|
|
30
|
+
-h|--help)
|
|
31
|
+
usage
|
|
32
|
+
exit 0
|
|
33
|
+
;;
|
|
34
|
+
*)
|
|
35
|
+
usage >&2
|
|
36
|
+
exit 64
|
|
37
|
+
;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
if [ -z "$ROOT" ]; then
|
|
42
|
+
ROOT="$(pwd)"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
46
|
+
ROOT="$(realpath "$ROOT")"
|
|
47
|
+
else
|
|
48
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [ ! -d "$ROOT" ]; then
|
|
52
|
+
printf 'ERROR: repo root does not exist: %s\n' "$ROOT" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
cd "$ROOT"
|
|
57
|
+
|
|
58
|
+
FAILURES=0
|
|
59
|
+
WARNINGS=0
|
|
60
|
+
RAN=0
|
|
61
|
+
|
|
62
|
+
section() {
|
|
63
|
+
printf '\n== %s ==\n' "$1"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pass() {
|
|
67
|
+
printf 'PASS [%s] %s\n' "$1" "$2"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
warn() {
|
|
71
|
+
WARNINGS=$((WARNINGS + 1))
|
|
72
|
+
printf 'WARN [%s] %s\n' "$1" "$2" >&2
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fail() {
|
|
76
|
+
FAILURES=$((FAILURES + 1))
|
|
77
|
+
printf 'FAIL [%s] %s\n' "$1" "$2" >&2
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
strict_or_warn() {
|
|
81
|
+
if [ "$STRICT" -eq 1 ]; then
|
|
82
|
+
fail "$1" "$2"
|
|
83
|
+
else
|
|
84
|
+
warn "$1" "$2"
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
run_cmd() {
|
|
89
|
+
local gate="$1"
|
|
90
|
+
local desc="$2"
|
|
91
|
+
shift 2
|
|
92
|
+
|
|
93
|
+
RAN=$((RAN + 1))
|
|
94
|
+
printf 'RUN [%s] %s\n' "$gate" "$desc"
|
|
95
|
+
printf 'CMD [%s] %s\n' "$gate" "$*"
|
|
96
|
+
|
|
97
|
+
if [ "$NO_RUN" -eq 1 ]; then
|
|
98
|
+
pass "$gate" "planned only (--no-run)"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if "$@"; then
|
|
103
|
+
pass "$gate" "$desc"
|
|
104
|
+
else
|
|
105
|
+
fail "$gate" "$desc"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
has_file() {
|
|
110
|
+
[ -f "$1" ]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
has_npm_script() {
|
|
114
|
+
local script_name="$1"
|
|
115
|
+
has_file package.json || return 1
|
|
116
|
+
command -v node >/dev/null 2>&1 || return 1
|
|
117
|
+
node -e '
|
|
118
|
+
const fs = require("node:fs");
|
|
119
|
+
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
120
|
+
process.exit(pkg.scripts && Object.prototype.hasOwnProperty.call(pkg.scripts, process.argv[1]) ? 0 : 1);
|
|
121
|
+
' "$script_name"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
run_npm_script_if_present() {
|
|
125
|
+
local script_name="$1"
|
|
126
|
+
local gate="$2"
|
|
127
|
+
if has_npm_script "$script_name"; then
|
|
128
|
+
run_cmd "$gate" "npm run $script_name" npm run "$script_name"
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
return 1
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
detect_python_surface() {
|
|
135
|
+
find . -maxdepth 3 \
|
|
136
|
+
\( -name 'requirements*.txt' -o -name 'pyproject.toml' -o -name 'poetry.lock' -o -name 'uv.lock' -o -name 'Pipfile.lock' \) \
|
|
137
|
+
-not -path './node_modules/*' \
|
|
138
|
+
-not -path './.git/*' \
|
|
139
|
+
-print
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
detect_requirements_files() {
|
|
143
|
+
find . -maxdepth 3 \
|
|
144
|
+
-name 'requirements*.txt' \
|
|
145
|
+
-not -path './node_modules/*' \
|
|
146
|
+
-not -path './.git/*' \
|
|
147
|
+
-print | sort
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
hook_file_mentions_reorg_guard() {
|
|
151
|
+
local file="$1"
|
|
152
|
+
[ -f "$file" ] || return 1
|
|
153
|
+
grep -q 'safedeps' "$file"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
safedeps_install_guard_present() {
|
|
157
|
+
[ -d "$HOME/.claude/skills/safedeps" ] && return 0
|
|
158
|
+
[ -d "$HOME/.codex/skills/safedeps" ] && return 0
|
|
159
|
+
hook_file_mentions_reorg_guard "$HOME/.claude/settings.json" && return 0
|
|
160
|
+
hook_file_mentions_reorg_guard "$HOME/.codex/hooks.json" && return 0
|
|
161
|
+
return 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
section "repo"
|
|
165
|
+
printf 'root: %s\n' "$ROOT"
|
|
166
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
167
|
+
pass repo "inside git worktree"
|
|
168
|
+
else
|
|
169
|
+
strict_or_warn repo "not inside a git worktree"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
if [ -f docs/security-release-gates.md ] || [ -f SECURITY.md ]; then
|
|
173
|
+
pass repo "release/security documentation present"
|
|
174
|
+
else
|
|
175
|
+
warn repo "no docs/security-release-gates.md or SECURITY.md"
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
section "secrets"
|
|
179
|
+
if run_npm_script_if_present "security:hooks:check" "secrets"; then
|
|
180
|
+
:
|
|
181
|
+
fi
|
|
182
|
+
if run_npm_script_if_present "security:scan:worktree" "secrets"; then
|
|
183
|
+
:
|
|
184
|
+
elif [ -x scripts/security/run-gitleaks.sh ]; then
|
|
185
|
+
run_cmd secrets "repo gitleaks wrapper" bash scripts/security/run-gitleaks.sh --worktree
|
|
186
|
+
elif command -v gitleaks >/dev/null 2>&1 && { [ -f .gitleaks.toml ] || [ -f gitleaks.toml ]; }; then
|
|
187
|
+
config=".gitleaks.toml"
|
|
188
|
+
[ -f "$config" ] || config="gitleaks.toml"
|
|
189
|
+
run_cmd secrets "gitleaks dir scan" gitleaks dir --no-banner --redact --verbose --config "$config" .
|
|
190
|
+
else
|
|
191
|
+
strict_or_warn secrets "no gitleaks gate detected"
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
section "node"
|
|
195
|
+
if has_file package.json; then
|
|
196
|
+
pass node "package.json detected"
|
|
197
|
+
if run_npm_script_if_present "security:audit" "node"; then
|
|
198
|
+
:
|
|
199
|
+
elif has_file package-lock.json || has_file npm-shrinkwrap.json; then
|
|
200
|
+
run_cmd node "npm audit --audit-level=moderate" npm audit --audit-level=moderate
|
|
201
|
+
else
|
|
202
|
+
strict_or_warn node "package.json exists but no npm lockfile/audit script was detected"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
if safedeps_install_guard_present; then
|
|
206
|
+
pass install-guard "safedeps appears installed/configured"
|
|
207
|
+
elif [ "$STRICT" -eq 1 ] || [ "${SECURITY_RELEASE_GATES_REQUIRE_INSTALL_GUARD:-0}" = "1" ]; then
|
|
208
|
+
fail install-guard "npm project has no detectable safedeps install-time guard"
|
|
209
|
+
else
|
|
210
|
+
warn install-guard "safedeps not detected; release gate can continue, install-time guard is separate"
|
|
211
|
+
fi
|
|
212
|
+
else
|
|
213
|
+
pass node "no package.json detected"
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
section "python"
|
|
217
|
+
PYTHON_SURFACE="$(detect_python_surface || true)"
|
|
218
|
+
if [ -z "$PYTHON_SURFACE" ]; then
|
|
219
|
+
pass python "no Python dependency surface detected"
|
|
220
|
+
elif [ -n "${SECURITY_RELEASE_GATES_PYTHON_AUDIT_COMMAND:-}" ]; then
|
|
221
|
+
run_cmd python "custom Python audit command" bash -lc "$SECURITY_RELEASE_GATES_PYTHON_AUDIT_COMMAND"
|
|
222
|
+
elif command -v pip-audit >/dev/null 2>&1; then
|
|
223
|
+
REQUIREMENTS="$(detect_requirements_files || true)"
|
|
224
|
+
if [ -n "$REQUIREMENTS" ]; then
|
|
225
|
+
while IFS= read -r requirements_file; do
|
|
226
|
+
[ -n "$requirements_file" ] || continue
|
|
227
|
+
run_cmd python "pip-audit $requirements_file" pip-audit -r "$requirements_file"
|
|
228
|
+
done <<EOF_REQ
|
|
229
|
+
$REQUIREMENTS
|
|
230
|
+
EOF_REQ
|
|
231
|
+
else
|
|
232
|
+
strict_or_warn python "Python lock/project files detected, but no requirements*.txt or repo-provided Python audit command exists"
|
|
233
|
+
fi
|
|
234
|
+
else
|
|
235
|
+
strict_or_warn python "Python dependency files detected, but pip-audit is not installed and no custom audit command was provided"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
section "ci"
|
|
239
|
+
if find .github/workflows -maxdepth 1 -type f 2>/dev/null | xargs grep -E 'security:|gitleaks|pip-audit|npm audit' >/dev/null 2>&1; then
|
|
240
|
+
pass ci "workflow appears to run security gates"
|
|
241
|
+
else
|
|
242
|
+
warn ci "no obvious GitHub security gate workflow detected"
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
section "summary"
|
|
246
|
+
printf 'gates_run=%s warnings=%s failures=%s strict=%s no_run=%s\n' "$RAN" "$WARNINGS" "$FAILURES" "$STRICT" "$NO_RUN"
|
|
247
|
+
|
|
248
|
+
if [ "$FAILURES" -gt 0 ]; then
|
|
249
|
+
exit 1
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
exit 0
|