@osfactory/har 0.53.0 → 0.54.1
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/dist/index.js +2 -2
- package/dist/templates/har-boilerplate/STAGES.md +3 -2
- package/dist/templates/har-boilerplate/agent-slot.sh +31 -0
- package/dist/templates/har-boilerplate-cli/STAGES.md +3 -2
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +31 -0
- package/dist/templates/har-boilerplate-ios/README.md +18 -0
- package/dist/templates/har-boilerplate-ios/STAGES.md +3 -2
- package/dist/templates/har-boilerplate-ios/agent-slot.sh +31 -0
- package/dist/templates/har-boilerplate-ios/setup-infra.sh +11 -0
- package/dist/templates/har-boilerplate-ios/simulator.sh +106 -14
- package/dist/templates/plugins/semgrep/.github/workflows/semgrep.yml +39 -0
- package/dist/templates/plugins/semgrep/.har/stages/SEMGREP.md +91 -0
- package/dist/templates/plugins/semgrep/.har/stages/sast.sh +141 -0
- package/dist/templates/plugins/semgrep/README.md +57 -0
- package/dist/templates/plugins/semgrep/template.manifest.json +44 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -69193,7 +69193,7 @@ function harnessExists(repoPath) {
|
|
|
69193
69193
|
// src/harness/plugins.ts
|
|
69194
69194
|
init_schema2();
|
|
69195
69195
|
init_stages();
|
|
69196
|
-
var PLUGIN_IDS = ["playwright", "rocketsim", "kerno", "gitleaks", "trivy"];
|
|
69196
|
+
var PLUGIN_IDS = ["playwright", "rocketsim", "kerno", "gitleaks", "trivy", "semgrep"];
|
|
69197
69197
|
var PluginManifestFileSchema = external_exports.object({
|
|
69198
69198
|
src: external_exports.string().min(1),
|
|
69199
69199
|
dest: external_exports.string().min(1),
|
|
@@ -70294,7 +70294,7 @@ var ADAPTATION_PROMPT_FILE = "ADAPT-PROMPT.md";
|
|
|
70294
70294
|
var PROFILE_HINTS = {
|
|
70295
70295
|
default: "Web app profile (SaaS/full-stack) \u2014 Docker Compose for shared infra (HARNESS_INFRA_SERVICES), PM2 for the primary application only, git worktree per agent slot by default. Launch provisions toolchain via harness.env (HARNESS_ECOSYSTEM, HARNESS_INSTALL_CMD) and writes paths to .env.agent.<id>. Identify the primary app agents modify; run supporting services shared.",
|
|
70296
70296
|
cli: "CLI/library profile (typical SWE-bench) \u2014 no PM2. Optional Docker Compose via HARNESS_INFRA_SERVICES. Git worktree by default. Launch provisions toolchain declaratively (HARNESS_ECOSYSTEM auto-detects common ecosystems); verify must use resolved tool paths from .env.agent.<id>, never hardcoded interpreter or package-manager paths.",
|
|
70297
|
-
ios: "iOS mobile app profile \u2014 xcodebuild + iOS Simulator in an isolated git worktree. Set HARNESS_XCODE_SCHEME, workspace/project, HARNESS_SIMULATOR_NAME, HARNESS_BUNDLE_ID. Launch writes XCODEBUILD_BIN to .env.agent.<id>; verify uses it.
|
|
70297
|
+
ios: "iOS mobile app profile \u2014 xcodebuild + iOS Simulator in an isolated git worktree. Set HARNESS_XCODE_SCHEME, workspace/project, HARNESS_SIMULATOR_NAME, HARNESS_BUNDLE_ID. Launch writes XCODEBUILD_BIN to .env.agent.<id>; verify uses it."
|
|
70298
70298
|
};
|
|
70299
70299
|
function loadTemplate(name) {
|
|
70300
70300
|
const filePath = resolveTemplateFile(name);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# HAR Stages — authoring guide
|
|
2
2
|
|
|
3
3
|
Stages are the harness's single vocabulary for runnable checks and lifecycle
|
|
4
|
-
actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`), your
|
|
4
|
+
actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`, `semgrep`), your
|
|
5
5
|
project's test/lint commands, bespoke validation scripts — registers in
|
|
6
6
|
`.har/stages.json` with the same schema, and agents interact with stages only
|
|
7
7
|
through that registry (CLI `har env verify`, MCP `har_run_stage` /
|
|
@@ -92,7 +92,8 @@ committed.
|
|
|
92
92
|
`har env add-plugin --list` shows available plugins; `har env add-plugin
|
|
93
93
|
playwright` (web), `har env add-plugin rocketsim` (iOS), or `har env
|
|
94
94
|
add-plugin kerno` (backend), `har env add-plugin gitleaks` (secrets scanning, any
|
|
95
|
-
stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack)
|
|
95
|
+
stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack),
|
|
96
|
+
or `har env add-plugin semgrep` (SAST, any stack)
|
|
96
97
|
installs one. A
|
|
97
98
|
plugin is just packaging: it copies files, merges `package.json` fragments,
|
|
98
99
|
and registers stages through the exact same registry as `add-stage --custom`.
|
|
@@ -445,6 +445,35 @@ try {
|
|
|
445
445
|
# optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
|
|
446
446
|
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
447
447
|
# SLOT_STATUS, SLOT_LAST_ERROR, SLOT_WORK_UNIT_ID, SLOT_ATTEMPT_ID
|
|
448
|
+
# Mission Control re-reads slot state only when something syncs, and a sync takes
|
|
449
|
+
# about twenty seconds — too long to make a slot change wait for it. Detached, and
|
|
450
|
+
# coalesced so a run of slot changes leaves one sync going, then one more if needed.
|
|
451
|
+
har_notify_control() {
|
|
452
|
+
local root bin lock
|
|
453
|
+
root="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
454
|
+
bin="$(command -v har 2>/dev/null || echo "${root}/node_modules/.bin/har")"
|
|
455
|
+
[ -x "$bin" ] || return 0
|
|
456
|
+
|
|
457
|
+
lock="${TMPDIR:-/tmp}/har-control-sync.$(id -u)"
|
|
458
|
+
: > "${lock}.pending"
|
|
459
|
+
mkdir "$lock" 2>/dev/null || return 0
|
|
460
|
+
(
|
|
461
|
+
trap "" HUP
|
|
462
|
+
trap 'rmdir "$lock" 2>/dev/null || true' EXIT
|
|
463
|
+
cd "$root" || exit 0
|
|
464
|
+
while [ -f "${lock}.pending" ]; do
|
|
465
|
+
rm -f "${lock}.pending"
|
|
466
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
467
|
+
timeout 120 "$bin" control sync || true
|
|
468
|
+
else
|
|
469
|
+
"$bin" control sync || true
|
|
470
|
+
fi
|
|
471
|
+
done
|
|
472
|
+
# Redirected as a whole, not per command: a background job that keeps the inherited
|
|
473
|
+
# stdout open makes any caller reading that pipe wait for it.
|
|
474
|
+
) >/dev/null 2>&1 &
|
|
475
|
+
}
|
|
476
|
+
|
|
448
477
|
write_slot_registry() {
|
|
449
478
|
local file
|
|
450
479
|
file="$(slot_registry_file "$SLOT_AGENT_ID")"
|
|
@@ -474,10 +503,12 @@ for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PR
|
|
|
474
503
|
}
|
|
475
504
|
fs.writeFileSync(process.argv[1], JSON.stringify(entry, null, 2) + "\n");
|
|
476
505
|
' "$file"
|
|
506
|
+
har_notify_control
|
|
477
507
|
}
|
|
478
508
|
|
|
479
509
|
remove_slot_registry() {
|
|
480
510
|
rm -f "$(slot_registry_file "$1")"
|
|
511
|
+
har_notify_control
|
|
481
512
|
}
|
|
482
513
|
|
|
483
514
|
# Exit 0 when the worktree has uncommitted or untracked changes.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# HAR Stages — authoring guide
|
|
2
2
|
|
|
3
3
|
Stages are the harness's single vocabulary for runnable checks and lifecycle
|
|
4
|
-
actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`), your
|
|
4
|
+
actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`, `semgrep`), your
|
|
5
5
|
project's test/lint commands, bespoke validation scripts — registers in
|
|
6
6
|
`.har/stages.json` with the same schema, and agents interact with stages only
|
|
7
7
|
through that registry (CLI `har env verify`, MCP `har_run_stage` /
|
|
@@ -92,7 +92,8 @@ committed.
|
|
|
92
92
|
`har env add-plugin --list` shows available plugins; `har env add-plugin
|
|
93
93
|
playwright` (web), `har env add-plugin rocketsim` (iOS), or `har env
|
|
94
94
|
add-plugin kerno` (backend), `har env add-plugin gitleaks` (secrets scanning, any
|
|
95
|
-
stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack)
|
|
95
|
+
stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack),
|
|
96
|
+
or `har env add-plugin semgrep` (SAST, any stack)
|
|
96
97
|
installs one. A
|
|
97
98
|
plugin is just packaging: it copies files, merges `package.json` fragments,
|
|
98
99
|
and registers stages through the exact same registry as `add-stage --custom`.
|
|
@@ -427,6 +427,35 @@ try {
|
|
|
427
427
|
# optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
|
|
428
428
|
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
429
429
|
# SLOT_STATUS, SLOT_LAST_ERROR, SLOT_WORK_UNIT_ID, SLOT_ATTEMPT_ID
|
|
430
|
+
# Mission Control re-reads slot state only when something syncs, and a sync takes
|
|
431
|
+
# about twenty seconds — too long to make a slot change wait for it. Detached, and
|
|
432
|
+
# coalesced so a run of slot changes leaves one sync going, then one more if needed.
|
|
433
|
+
har_notify_control() {
|
|
434
|
+
local root bin lock
|
|
435
|
+
root="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
436
|
+
bin="$(command -v har 2>/dev/null || echo "${root}/node_modules/.bin/har")"
|
|
437
|
+
[ -x "$bin" ] || return 0
|
|
438
|
+
|
|
439
|
+
lock="${TMPDIR:-/tmp}/har-control-sync.$(id -u)"
|
|
440
|
+
: > "${lock}.pending"
|
|
441
|
+
mkdir "$lock" 2>/dev/null || return 0
|
|
442
|
+
(
|
|
443
|
+
trap "" HUP
|
|
444
|
+
trap 'rmdir "$lock" 2>/dev/null || true' EXIT
|
|
445
|
+
cd "$root" || exit 0
|
|
446
|
+
while [ -f "${lock}.pending" ]; do
|
|
447
|
+
rm -f "${lock}.pending"
|
|
448
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
449
|
+
timeout 120 "$bin" control sync || true
|
|
450
|
+
else
|
|
451
|
+
"$bin" control sync || true
|
|
452
|
+
fi
|
|
453
|
+
done
|
|
454
|
+
# Redirected as a whole, not per command: a background job that keeps the inherited
|
|
455
|
+
# stdout open makes any caller reading that pipe wait for it.
|
|
456
|
+
) >/dev/null 2>&1 &
|
|
457
|
+
}
|
|
458
|
+
|
|
430
459
|
write_slot_registry() {
|
|
431
460
|
local file
|
|
432
461
|
file="$(slot_registry_file "$SLOT_AGENT_ID")"
|
|
@@ -456,10 +485,12 @@ for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PR
|
|
|
456
485
|
}
|
|
457
486
|
fs.writeFileSync(process.argv[1], JSON.stringify(entry, null, 2) + "\n");
|
|
458
487
|
' "$file"
|
|
488
|
+
har_notify_control
|
|
459
489
|
}
|
|
460
490
|
|
|
461
491
|
remove_slot_registry() {
|
|
462
492
|
rm -f "$(slot_registry_file "$1")"
|
|
493
|
+
har_notify_control
|
|
463
494
|
}
|
|
464
495
|
|
|
465
496
|
# Exit 0 when the worktree has uncommitted or untracked changes.
|
|
@@ -109,6 +109,24 @@ launch fails and lists the models this machine can create.
|
|
|
109
109
|
If `HARNESS_SIMULATOR_NAME` is not a model but names an existing device, that
|
|
110
110
|
device is used as-is and never deleted — how a hand-renamed simulator is targeted.
|
|
111
111
|
|
|
112
|
+
### Running from an agent sandbox
|
|
113
|
+
|
|
114
|
+
`xcrun simctl` reaches CoreSimulatorService over XPC. Coding agents that sandbox
|
|
115
|
+
their shell (Codex, Claude Code and others) usually deny that lookup, so `simctl`
|
|
116
|
+
fails while `xcodebuild` keeps working — every simulator command then dies with
|
|
117
|
+
`CoreSimulatorService connection became invalid`. Nothing is missing on the machine.
|
|
118
|
+
|
|
119
|
+
Launch and teardown say so when it happens. Either run `har env launch <id>` and
|
|
120
|
+
`har env teardown <id>` from a normal terminal and let the agent work in the
|
|
121
|
+
worktree, or grant the agent unsandboxed access to `xcrun simctl` — the escalation
|
|
122
|
+
must cover every subcommand the harness uses (`list`, `create`, `boot`,
|
|
123
|
+
`bootstatus`, `shutdown`, `delete`), not just `list`.
|
|
124
|
+
|
|
125
|
+
The same messages distinguish a second cause: if `xcrun simctl` is missing from
|
|
126
|
+
the selected developer directory — no Xcode, or `xcode-select` pointing at the
|
|
127
|
+
Command Line Tools — they say so and print the current selection instead of
|
|
128
|
+
blaming a sandbox.
|
|
129
|
+
|
|
112
130
|
The device lands in `.env.agent.<id>` as `HARNESS_SIMULATOR_UDID`,
|
|
113
131
|
`HARNESS_SIMULATOR_DEVICE_NAME` and `HARNESS_IOS_DESTINATION` — the model stays in
|
|
114
132
|
`HARNESS_SIMULATOR_NAME`, so the two never mean the same thing in one place; what a slot holds is tracked in `.har/simulators/`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# HAR Stages — authoring guide
|
|
2
2
|
|
|
3
3
|
Stages are the harness's single vocabulary for runnable checks and lifecycle
|
|
4
|
-
actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`), your
|
|
4
|
+
actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`, `semgrep`), your
|
|
5
5
|
project's test/lint commands, bespoke validation scripts — registers in
|
|
6
6
|
`.har/stages.json` with the same schema, and agents interact with stages only
|
|
7
7
|
through that registry (CLI `har env verify`, MCP `har_run_stage` /
|
|
@@ -92,7 +92,8 @@ committed.
|
|
|
92
92
|
`har env add-plugin --list` shows available plugins; `har env add-plugin
|
|
93
93
|
playwright` (web), `har env add-plugin rocketsim` (iOS), or `har env
|
|
94
94
|
add-plugin kerno` (backend), `har env add-plugin gitleaks` (secrets scanning, any
|
|
95
|
-
stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack)
|
|
95
|
+
stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack),
|
|
96
|
+
or `har env add-plugin semgrep` (SAST, any stack)
|
|
96
97
|
installs one. A
|
|
97
98
|
plugin is just packaging: it copies files, merges `package.json` fragments,
|
|
98
99
|
and registers stages through the exact same registry as `add-stage --custom`.
|
|
@@ -95,6 +95,35 @@ try {
|
|
|
95
95
|
# optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
|
|
96
96
|
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
97
97
|
# SLOT_STATUS, SLOT_LAST_ERROR, SLOT_WORK_UNIT_ID, SLOT_ATTEMPT_ID
|
|
98
|
+
# Mission Control re-reads slot state only when something syncs, and a sync takes
|
|
99
|
+
# about twenty seconds — too long to make a slot change wait for it. Detached, and
|
|
100
|
+
# coalesced so a run of slot changes leaves one sync going, then one more if needed.
|
|
101
|
+
har_notify_control() {
|
|
102
|
+
local root bin lock
|
|
103
|
+
root="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
104
|
+
bin="$(command -v har 2>/dev/null || echo "${root}/node_modules/.bin/har")"
|
|
105
|
+
[ -x "$bin" ] || return 0
|
|
106
|
+
|
|
107
|
+
lock="${TMPDIR:-/tmp}/har-control-sync.$(id -u)"
|
|
108
|
+
: > "${lock}.pending"
|
|
109
|
+
mkdir "$lock" 2>/dev/null || return 0
|
|
110
|
+
(
|
|
111
|
+
trap "" HUP
|
|
112
|
+
trap 'rmdir "$lock" 2>/dev/null || true' EXIT
|
|
113
|
+
cd "$root" || exit 0
|
|
114
|
+
while [ -f "${lock}.pending" ]; do
|
|
115
|
+
rm -f "${lock}.pending"
|
|
116
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
117
|
+
timeout 120 "$bin" control sync || true
|
|
118
|
+
else
|
|
119
|
+
"$bin" control sync || true
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
# Redirected as a whole, not per command: a background job that keeps the inherited
|
|
123
|
+
# stdout open makes any caller reading that pipe wait for it.
|
|
124
|
+
) >/dev/null 2>&1 &
|
|
125
|
+
}
|
|
126
|
+
|
|
98
127
|
write_slot_registry() {
|
|
99
128
|
local file
|
|
100
129
|
file="$(slot_registry_file "$SLOT_AGENT_ID")"
|
|
@@ -124,10 +153,12 @@ for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PR
|
|
|
124
153
|
}
|
|
125
154
|
fs.writeFileSync(process.argv[1], JSON.stringify(entry, null, 2) + "\n");
|
|
126
155
|
' "$file"
|
|
156
|
+
har_notify_control
|
|
127
157
|
}
|
|
128
158
|
|
|
129
159
|
remove_slot_registry() {
|
|
130
160
|
rm -f "$(slot_registry_file "$1")"
|
|
161
|
+
har_notify_control
|
|
131
162
|
}
|
|
132
163
|
|
|
133
164
|
# Exit 0 when the worktree has uncommitted or untracked changes.
|
|
@@ -19,6 +19,14 @@ source "$SCRIPT_DIR/simulator.sh"
|
|
|
19
19
|
|
|
20
20
|
log() { echo "==> $*" >&2; }
|
|
21
21
|
|
|
22
|
+
# simctl could not be asked, so nothing it did not return is evidence of anything
|
|
23
|
+
# missing on this machine. The wording lives in simulator.sh so launch and this
|
|
24
|
+
# script cannot drift apart on which of the two causes they name.
|
|
25
|
+
fail_simctl_unavailable() {
|
|
26
|
+
har_sim_log_simctl_unavailable
|
|
27
|
+
exit 1
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
# ── Xcode check ───────────────────────────────────────────────────────────────
|
|
23
31
|
if ! command -v xcodebuild >/dev/null 2>&1; then
|
|
24
32
|
echo "Error: xcodebuild not found — install Xcode from the App Store." >&2
|
|
@@ -40,6 +48,8 @@ if har_sim_per_slot_enabled; then
|
|
|
40
48
|
PLAN_STATUS="${PLAN%%$'\t'*}"
|
|
41
49
|
if [ "$PLAN_STATUS" = "OK" ]; then
|
|
42
50
|
log "Per-slot simulators: launch creates one $(printf '%s' "$PLAN" | cut -f3) per agent."
|
|
51
|
+
elif [ "$PLAN_STATUS" = "SIMCTL_UNAVAILABLE" ]; then
|
|
52
|
+
fail_simctl_unavailable
|
|
43
53
|
elif [ "$PLAN_STATUS" = "NO_MODEL" ] && [ -n "$(har_sim_device_by_name "${HARNESS_SIMULATOR_NAME:-}")" ]; then
|
|
44
54
|
# Supported fallback: the name is an existing device rather than a model.
|
|
45
55
|
log "Per-slot simulators: launch reuses the existing device '${SIMULATOR_NAME}'."
|
|
@@ -63,6 +73,7 @@ else
|
|
|
63
73
|
UDID="${RESOLVED%%$'\t'*}"
|
|
64
74
|
|
|
65
75
|
if [ -z "$UDID" ]; then
|
|
76
|
+
har_sim_simctl_available || fail_simctl_unavailable
|
|
66
77
|
echo "Error: Simulator '${SIMULATOR_NAME}' not found in available devices." >&2
|
|
67
78
|
echo " Available simulators:" >&2
|
|
68
79
|
xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone|iPad' | head -20 >&2
|
|
@@ -108,10 +108,25 @@ har_sim_claim_owner_of() {
|
|
|
108
108
|
|
|
109
109
|
# ── Device discovery ──────────────────────────────────────────────────────────
|
|
110
110
|
|
|
111
|
+
# Every simctl call goes through here, so "simctl could not be asked" stays
|
|
112
|
+
# distinguishable from "simctl answered, and the answer is empty" at the one place
|
|
113
|
+
# that still knows the difference. Non-zero means the first; callers check it
|
|
114
|
+
# instead of reading an empty list as a fact about the machine.
|
|
115
|
+
har_sim_simctl() {
|
|
116
|
+
local out rc=0
|
|
117
|
+
out="$(xcrun simctl "$@" 2>/dev/null)" || rc=$?
|
|
118
|
+
if [ "$rc" -ne 0 ] || [ -z "$out" ]; then
|
|
119
|
+
return 1
|
|
120
|
+
fi
|
|
121
|
+
printf '%s' "$out"
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
# Unavailable devices are skipped: a device whose runtime was uninstalled must
|
|
112
125
|
# not pass validation only to fail at boot.
|
|
113
126
|
har_sim_device_field() {
|
|
114
|
-
|
|
127
|
+
local devices
|
|
128
|
+
devices="$(har_sim_simctl list devices --json)" || return 1
|
|
129
|
+
printf '%s' "$devices" | node -e '
|
|
115
130
|
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
116
131
|
const [want, field] = process.argv.slice(1);
|
|
117
132
|
const device = Object.values(data.devices || {})
|
|
@@ -128,9 +143,10 @@ har_sim_device_name() { har_sim_device_field "$1" name; }
|
|
|
128
143
|
# UDID of an existing device with this exact name, newest runtime first.
|
|
129
144
|
# A substring match would resolve "iPhone 16" to "iPhone 16 Pro Max".
|
|
130
145
|
har_sim_device_by_name() {
|
|
131
|
-
local want="${1:-}"
|
|
146
|
+
local want="${1:-}" devices
|
|
132
147
|
[ -n "$want" ] || return 0
|
|
133
|
-
|
|
148
|
+
devices="$(har_sim_simctl list devices available --json)" || return 1
|
|
149
|
+
printf '%s' "$devices" | node -e '
|
|
134
150
|
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
135
151
|
const want = process.argv[1];
|
|
136
152
|
const rows = [];
|
|
@@ -157,22 +173,66 @@ har_sim_resolve_configured() {
|
|
|
157
173
|
printf '%s\t%s' "$udid" "${HARNESS_SIMULATOR_NAME}"
|
|
158
174
|
}
|
|
159
175
|
|
|
160
|
-
# `simctl list` is a subprocess and a JSON parse
|
|
161
|
-
#
|
|
176
|
+
# `simctl list` is a subprocess and a JSON parse, so each answer is cached — but in
|
|
177
|
+
# the calling shell, not the process: `$(har_sim_runtimes_json)` fetches, then throws
|
|
178
|
+
# the cache and the availability flag away with the subshell that held them. Call
|
|
179
|
+
# these directly and read the variable whenever either one matters.
|
|
162
180
|
har_sim_devicetypes_json() {
|
|
163
181
|
if [ -z "${_HAR_SIM_TYPES_JSON:-}" ]; then
|
|
164
|
-
_HAR_SIM_TYPES_JSON="$(
|
|
182
|
+
_HAR_SIM_TYPES_JSON="$(har_sim_simctl list devicetypes --json)" || _HAR_SIM_TYPES_JSON='{}'
|
|
165
183
|
fi
|
|
166
184
|
printf '%s' "$_HAR_SIM_TYPES_JSON"
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
har_sim_runtimes_json() {
|
|
170
188
|
if [ -z "${_HAR_SIM_RUNTIMES_JSON:-}" ]; then
|
|
171
|
-
_HAR_SIM_RUNTIMES_JSON="$(
|
|
189
|
+
if _HAR_SIM_RUNTIMES_JSON="$(har_sim_simctl list runtimes --json)"; then
|
|
190
|
+
_HAR_SIM_SIMCTL_OK=true
|
|
191
|
+
else
|
|
192
|
+
_HAR_SIM_RUNTIMES_JSON='{}'
|
|
193
|
+
_HAR_SIM_SIMCTL_OK=false
|
|
194
|
+
# A second call, but only ever when simctl is already failing — and failing
|
|
195
|
+
# is the fast path. Its stderr is the only thing that tells the causes apart.
|
|
196
|
+
_HAR_SIM_SIMCTL_ERROR="$(xcrun simctl list runtimes --json 2>&1 >/dev/null || true)"
|
|
197
|
+
fi
|
|
172
198
|
fi
|
|
173
199
|
printf '%s' "$_HAR_SIM_RUNTIMES_JSON"
|
|
174
200
|
}
|
|
175
201
|
|
|
202
|
+
# Ask simctl once, recording in this shell whether it answered and why not.
|
|
203
|
+
har_sim_probe_simctl() {
|
|
204
|
+
har_sim_runtimes_json >/dev/null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
har_sim_simctl_available() {
|
|
208
|
+
har_sim_probe_simctl
|
|
209
|
+
[ "${_HAR_SIM_SIMCTL_OK:-true}" = "true" ]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# Two unrelated problems make simctl unusable — a sandbox denying the
|
|
213
|
+
# CoreSimulatorService lookup, and a developer directory with no simctl in it.
|
|
214
|
+
# Naming the wrong one costs the reader the hunt this check exists to prevent.
|
|
215
|
+
har_sim_log_simctl_unavailable() {
|
|
216
|
+
# Teardown hits two failures in a row; the cause only needs saying once.
|
|
217
|
+
[ -z "${_HAR_SIM_SIMCTL_CAUSE_REPORTED:-}" ] || return 0
|
|
218
|
+
_HAR_SIM_SIMCTL_CAUSE_REPORTED=1
|
|
219
|
+
# For a launch the failing call happened inside the command substitution around
|
|
220
|
+
# har_sim_plan_creation, so the reason died with that subshell — ask again here.
|
|
221
|
+
har_sim_probe_simctl
|
|
222
|
+
case "${_HAR_SIM_SIMCTL_ERROR:-}" in
|
|
223
|
+
*"not a developer tool"*|*"unable to find utility"*|*"command not found"*)
|
|
224
|
+
har_sim_log "'xcrun simctl' is not in the selected developer directory"
|
|
225
|
+
har_sim_log " install Xcode, then point the toolchain at it:"
|
|
226
|
+
har_sim_log " sudo xcode-select -s /Applications/Xcode.app"
|
|
227
|
+
har_sim_log " current selection: $(xcode-select -p 2>/dev/null || echo 'none')"
|
|
228
|
+
return
|
|
229
|
+
;;
|
|
230
|
+
esac
|
|
231
|
+
har_sim_log "cannot reach CoreSimulatorService — 'xcrun simctl' is unusable here"
|
|
232
|
+
har_sim_log " this usually means the command is running inside an agent sandbox"
|
|
233
|
+
har_sim_log " run it from a normal terminal, or let the agent run 'xcrun simctl' unsandboxed"
|
|
234
|
+
}
|
|
235
|
+
|
|
176
236
|
# Model and runtime to create this slot's device from. Failures carry the models
|
|
177
237
|
# this machine can create, so the caller never has to re-derive that list.
|
|
178
238
|
# Prints one of:
|
|
@@ -180,10 +240,18 @@ har_sim_runtimes_json() {
|
|
|
180
240
|
# NO_RUNTIME no iOS runtime installed
|
|
181
241
|
# NO_MODEL <models> HARNESS_SIMULATOR_NAME is not a device model
|
|
182
242
|
# NO_RUNTIME_FOR_MODEL <models> model exists, no installed runtime supports it
|
|
243
|
+
# SIMCTL_UNAVAILABLE simctl could not be asked at all
|
|
183
244
|
har_sim_plan_creation() {
|
|
184
245
|
local types runtimes
|
|
185
246
|
types="$(har_sim_devicetypes_json)"
|
|
186
|
-
|
|
247
|
+
# Not `$(...)`: that would discard the availability flag with the subshell.
|
|
248
|
+
har_sim_runtimes_json >/dev/null
|
|
249
|
+
runtimes="$_HAR_SIM_RUNTIMES_JSON"
|
|
250
|
+
# Checked before the empty lists are read as an answer about the machine.
|
|
251
|
+
if ! har_sim_simctl_available; then
|
|
252
|
+
printf 'SIMCTL_UNAVAILABLE'
|
|
253
|
+
return
|
|
254
|
+
fi
|
|
187
255
|
node -e '
|
|
188
256
|
const [typesRaw, runtimesRaw, wantName, family] = process.argv.slice(1);
|
|
189
257
|
const types = JSON.parse(typesRaw).devicetypes || [];
|
|
@@ -240,7 +308,9 @@ process.stdout.write(plan());
|
|
|
240
308
|
# Every device whose name is <prefix> or starts with "<prefix>-", so a slot's
|
|
241
309
|
# leftovers are found whatever model they were created from.
|
|
242
310
|
har_sim_devices_with_prefix() {
|
|
243
|
-
|
|
311
|
+
local devices
|
|
312
|
+
devices="$(har_sim_simctl list devices --json)" || return 1
|
|
313
|
+
printf '%s' "$devices" | node -e '
|
|
244
314
|
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
245
315
|
const prefix = process.argv[1];
|
|
246
316
|
const matches = Object.values(data.devices || {})
|
|
@@ -252,12 +322,19 @@ process.stdout.write(matches.join("\n"));
|
|
|
252
322
|
' "$1" 2>/dev/null || true
|
|
253
323
|
}
|
|
254
324
|
|
|
325
|
+
# Returns 0 even when it could not look: teardown.sh runs under `set -e`, and
|
|
326
|
+
# aborting there would leave the slot registry behind on top of the device.
|
|
255
327
|
har_sim_delete_devices_with_prefix() {
|
|
256
|
-
local udid
|
|
328
|
+
local udid list
|
|
329
|
+
if ! list="$(har_sim_devices_with_prefix "$1")"; then
|
|
330
|
+
har_sim_log "cannot check for leftover devices named ${1}* — none were removed"
|
|
331
|
+
har_sim_log_simctl_unavailable
|
|
332
|
+
return 0
|
|
333
|
+
fi
|
|
257
334
|
while read -r udid; do
|
|
258
335
|
[ -n "$udid" ] || continue
|
|
259
336
|
xcrun simctl delete "$udid" >/dev/null 2>&1 || true
|
|
260
|
-
done <<< "$
|
|
337
|
+
done <<< "$list"
|
|
261
338
|
}
|
|
262
339
|
|
|
263
340
|
# ── Boot ──────────────────────────────────────────────────────────────────────
|
|
@@ -289,7 +366,11 @@ _har_sim_select() {
|
|
|
289
366
|
|
|
290
367
|
# An explicit UDID is a deliberate override — never fall back off it.
|
|
291
368
|
if [ -n "${HARNESS_SIMULATOR_UDID:-}" ]; then
|
|
292
|
-
|
|
369
|
+
# A failed lookup is "could not ask"; an empty one is "no such device".
|
|
370
|
+
name="$(har_sim_device_name "$HARNESS_SIMULATOR_UDID")" || {
|
|
371
|
+
har_sim_log_simctl_unavailable
|
|
372
|
+
return 1
|
|
373
|
+
}
|
|
293
374
|
if [ -z "$name" ]; then
|
|
294
375
|
har_sim_log "HARNESS_SIMULATOR_UDID=${HARNESS_SIMULATOR_UDID} is not a known device"
|
|
295
376
|
return 1
|
|
@@ -310,6 +391,11 @@ _har_sim_select() {
|
|
|
310
391
|
models=""
|
|
311
392
|
[ "$plan" != "$status" ] && models="${plan#*$'\t'}"
|
|
312
393
|
|
|
394
|
+
if [ "$status" = "SIMCTL_UNAVAILABLE" ]; then
|
|
395
|
+
har_sim_log_simctl_unavailable
|
|
396
|
+
return 1
|
|
397
|
+
fi
|
|
398
|
+
|
|
313
399
|
if [ "$status" = "OK" ]; then
|
|
314
400
|
IFS=$'\t' read -r _ device_type model runtime _ <<< "$plan"
|
|
315
401
|
label="$(har_sim_device_label "$agent_id" "$model")"
|
|
@@ -388,8 +474,14 @@ har_sim_release() {
|
|
|
388
474
|
rm -f "$file"
|
|
389
475
|
if [ "$created" = "true" ] && [ -n "$udid" ]; then
|
|
390
476
|
xcrun simctl shutdown "$udid" >/dev/null 2>&1 || true
|
|
391
|
-
xcrun simctl delete "$udid" >/dev/null 2>&1
|
|
392
|
-
|
|
477
|
+
if xcrun simctl delete "$udid" >/dev/null 2>&1; then
|
|
478
|
+
echo "✓ Deleted simulator created for this slot ($udid)"
|
|
479
|
+
else
|
|
480
|
+
# Naming it is the whole remedy left: the claim is gone, so only the name
|
|
481
|
+
# prefix sweep at the next launch can still find this device.
|
|
482
|
+
har_sim_log "could not delete ${udid}, created for agent ${agent_id} — it is still on this machine"
|
|
483
|
+
har_sim_simctl_available || har_sim_log_simctl_unavailable
|
|
484
|
+
fi
|
|
393
485
|
fi
|
|
394
486
|
fi
|
|
395
487
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Semgrep SAST — official recipe (https://semgrep.dev/docs/semgrep-ci/sample-ci-configs)
|
|
2
|
+
#
|
|
3
|
+
# With a SEMGREP_APP_TOKEN secret (Semgrep AppSec Platform → Settings), `semgrep ci`
|
|
4
|
+
# publishes findings to the AppSec Platform — the org-level evidence layer that
|
|
5
|
+
# compliance tools like Vanta ingest natively:
|
|
6
|
+
# https://help.vanta.com/en/articles/15705377-connecting-vanta-semgrep
|
|
7
|
+
# Without the token, this falls back to a Community Edition scan that still blocks
|
|
8
|
+
# the PR on findings but publishes nothing.
|
|
9
|
+
name: Semgrep
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
pull_request: {}
|
|
13
|
+
push:
|
|
14
|
+
branches: ["main", "master"]
|
|
15
|
+
workflow_dispatch: {}
|
|
16
|
+
schedule:
|
|
17
|
+
# Daily scan so new registry rules surface without a code change
|
|
18
|
+
- cron: '17 5 * * *'
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
semgrep:
|
|
22
|
+
name: semgrep/ci
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
container:
|
|
25
|
+
image: semgrep/semgrep
|
|
26
|
+
if: (github.actor != 'dependabot[bot]')
|
|
27
|
+
steps:
|
|
28
|
+
# Pinned to a SHA so semgrep's own mutable-action-tag rule stays quiet
|
|
29
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
30
|
+
- name: Run Semgrep
|
|
31
|
+
env:
|
|
32
|
+
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
|
33
|
+
run: |
|
|
34
|
+
if [ -n "$SEMGREP_APP_TOKEN" ]; then
|
|
35
|
+
semgrep ci
|
|
36
|
+
else
|
|
37
|
+
echo "SEMGREP_APP_TOKEN not set — running Community Edition scan (no platform/Vanta reporting)"
|
|
38
|
+
semgrep scan --config auto --error
|
|
39
|
+
fi
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Semgrep SAST Stage
|
|
2
|
+
|
|
3
|
+
The `sast` stage scans the agent's session worktree with [Semgrep](https://semgrep.dev)
|
|
4
|
+
and fails verification when blocking findings exist. Full verify (`verify --full`)
|
|
5
|
+
runs it automatically once it is listed in `verificationStages`.
|
|
6
|
+
|
|
7
|
+
## Running
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Whole worktree
|
|
11
|
+
./.har/stages/sast.sh 1
|
|
12
|
+
|
|
13
|
+
# Only specific paths
|
|
14
|
+
./.har/stages/sast.sh 1 src/ lib/auth.py
|
|
15
|
+
|
|
16
|
+
# Included in full verify
|
|
17
|
+
./.har/verify.sh 1 --full
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Reports land under `.har/artifacts/sast/` in the **main repo root** (not the worktree):
|
|
21
|
+
|
|
22
|
+
| File | Contents |
|
|
23
|
+
|------|----------|
|
|
24
|
+
| `semgrep.json` | Full findings report (machine-readable) |
|
|
25
|
+
| `semgrep.sarif` | SARIF report — upload to GitHub code scanning if desired |
|
|
26
|
+
| `scan.log` | Semgrep's own text output for the run |
|
|
27
|
+
|
|
28
|
+
Exit codes pass through from Semgrep: `0` clean, `1` findings, `>=2` scan error.
|
|
29
|
+
|
|
30
|
+
## Choosing rules
|
|
31
|
+
|
|
32
|
+
The stage runs `semgrep scan --config "$HARNESS_SEMGREP_CONFIG"` (default `auto`).
|
|
33
|
+
Set the variable in `.har/harness.env` to pin behavior:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# .har/harness.env
|
|
37
|
+
export HARNESS_SEMGREP_CONFIG="p/ci" # curated low-noise ruleset
|
|
38
|
+
# export HARNESS_SEMGREP_CONFIG="p/security-audit"
|
|
39
|
+
# export HARNESS_SEMGREP_CONFIG=".semgrep/" # local rules only (works offline)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Honest notes about `--config auto` and registry rulesets (`p/...`):
|
|
43
|
+
|
|
44
|
+
- They **fetch rules from the Semgrep registry**, so the stage needs network access.
|
|
45
|
+
- Semgrep **sends pseudonymized metrics** to semgrep.dev when registry configs are
|
|
46
|
+
used (this cannot be disabled for registry configs). For fully offline, metric-free
|
|
47
|
+
scans, commit local rule files and point `HARNESS_SEMGREP_CONFIG` at them.
|
|
48
|
+
- `auto` picks rulesets based on the languages it detects — convenient, but pin a
|
|
49
|
+
named ruleset (`p/ci`) when you want reproducible results.
|
|
50
|
+
|
|
51
|
+
## Tuning noise
|
|
52
|
+
|
|
53
|
+
- Semgrep respects `.gitignore` by default; add a [`.semgrepignore`](https://semgrep.dev/docs/ignoring-files-folders-code)
|
|
54
|
+
file for extra exclusions (vendored code, generated files, fixtures).
|
|
55
|
+
- The stage always passes `--exclude .har` — harness scaffolding is not your
|
|
56
|
+
project's code and semgrep cannot parse some of its bash.
|
|
57
|
+
- Suppress a single finding with a trailing `# nosemgrep: <rule-id>` comment —
|
|
58
|
+
prefer rule-scoped suppressions over blanket `# nosemgrep`.
|
|
59
|
+
- Write repo-specific rules under `.semgrep/` and add the directory to your config.
|
|
60
|
+
|
|
61
|
+
## CI and compliance (Vanta)
|
|
62
|
+
|
|
63
|
+
Of the security plugins, this one has the strongest compliance story. The optional
|
|
64
|
+
workflow `.github/workflows/semgrep.yml` runs `semgrep ci` in the official
|
|
65
|
+
`semgrep/semgrep` container. With a `SEMGREP_APP_TOKEN` secret (generated in
|
|
66
|
+
Semgrep AppSec Platform → Settings), findings publish to the
|
|
67
|
+
[Semgrep AppSec Platform](https://semgrep.dev/login) — which has a
|
|
68
|
+
[native Vanta integration](https://help.vanta.com/en/articles/15705377-connecting-vanta-semgrep):
|
|
69
|
+
Vanta pulls findings, projects, and scan history from Semgrep automatically as
|
|
70
|
+
compliance evidence.
|
|
71
|
+
|
|
72
|
+
Division of labor:
|
|
73
|
+
|
|
74
|
+
- **This local stage** is the shift-left layer — agents catch findings *before*
|
|
75
|
+
code lands on the default branch. Local runs are invisible to Vanta (no noise).
|
|
76
|
+
- **The CI run** (`semgrep ci` on the default branch and PRs) is the org-level
|
|
77
|
+
evidence layer Vanta reads.
|
|
78
|
+
|
|
79
|
+
Without a token, the workflow falls back to `semgrep scan --config auto --error`
|
|
80
|
+
(Community Edition — CI still blocks on findings, but nothing publishes to the
|
|
81
|
+
platform and there is no Vanta evidence).
|
|
82
|
+
|
|
83
|
+
## Adapting per repo
|
|
84
|
+
|
|
85
|
+
- Pin `HARNESS_SEMGREP_CONFIG` to the ruleset your team standardizes on.
|
|
86
|
+
- Add `.semgrepignore` entries for paths that produce noise.
|
|
87
|
+
- If findings should not block agents yet, keep `sast` out of `verificationStages`
|
|
88
|
+
in `.har/stages.json` and run it manually while you triage the baseline.
|
|
89
|
+
- The stage summarizes reports with `node`; it assumes a Node.js toolchain is
|
|
90
|
+
available (true for HAR-provisioned slots on node ecosystems — on other stacks,
|
|
91
|
+
ensure `node` is on PATH or adapt the summary block).
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Semgrep SAST scan for an agent slot.
|
|
3
|
+
# Scans the session worktree with Semgrep and writes JSON + SARIF reports.
|
|
4
|
+
# Outputs JSON to stdout, human-readable progress to stderr.
|
|
5
|
+
#
|
|
6
|
+
# Usage: ./.har/stages/sast.sh <agent-id> [paths...]
|
|
7
|
+
# paths (optional): scan only these paths instead of the whole worktree
|
|
8
|
+
#
|
|
9
|
+
# Config: HARNESS_SEMGREP_CONFIG (default "auto") — any semgrep --config value,
|
|
10
|
+
# e.g. "p/ci", "p/security-audit", or a local rules file/dir. Set it in
|
|
11
|
+
# .har/harness.env to pin rulesets. Registry configs (auto, p/...) need
|
|
12
|
+
# network access and send pseudonymized metrics to semgrep.dev.
|
|
13
|
+
#
|
|
14
|
+
# Prerequisite: ./.har/launch.sh <agent-id> AND the semgrep CLI installed
|
|
15
|
+
# See: ./.har/stages/SEMGREP.md for the full adaptation guide.
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
HARNESS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
20
|
+
REPO_ROOT="$(cd "$HARNESS_DIR/.." && pwd)"
|
|
21
|
+
# agent-slot.sh expects SCRIPT_DIR to be .har/ (slot registry lives there)
|
|
22
|
+
SCRIPT_DIR="$HARNESS_DIR"
|
|
23
|
+
|
|
24
|
+
# shellcheck source=/dev/null
|
|
25
|
+
source "$HARNESS_DIR/harness.env"
|
|
26
|
+
# shellcheck source=/dev/null
|
|
27
|
+
source "$HARNESS_DIR/agent-slot.sh"
|
|
28
|
+
|
|
29
|
+
AGENT_ID="${1:?Usage: sast.sh <agent-id> [paths...]}"
|
|
30
|
+
shift
|
|
31
|
+
SCAN_PATHS=("$@")
|
|
32
|
+
|
|
33
|
+
validate_agent_id "$AGENT_ID"
|
|
34
|
+
|
|
35
|
+
log() { echo "==> [semgrep agent-$AGENT_ID] $*" >&2; }
|
|
36
|
+
|
|
37
|
+
# ── Preflight ─────────────────────────────────────────────────────────────────
|
|
38
|
+
if ! command -v semgrep >/dev/null 2>&1; then
|
|
39
|
+
echo "Error: semgrep CLI not found." >&2
|
|
40
|
+
echo " Install with one of:" >&2
|
|
41
|
+
echo " pipx install semgrep # recommended" >&2
|
|
42
|
+
echo " pip install semgrep" >&2
|
|
43
|
+
echo " brew install semgrep # may lag behind the latest release" >&2
|
|
44
|
+
echo " Then re-run: ./.har/stages/sast.sh ${AGENT_ID}" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# ── Resolve agent env ─────────────────────────────────────────────────────────
|
|
49
|
+
ENV_FILE="$(resolve_agent_env_file "$AGENT_ID" "$REPO_ROOT")" || {
|
|
50
|
+
echo "No .env.agent.${AGENT_ID} found." >&2
|
|
51
|
+
har_suggest_launch "$AGENT_ID" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# .env.agent.<id> exports REPO_ROOT pointing at the worktree — keep the main
|
|
56
|
+
# checkout path so artifacts land in the main repo root, not the worktree.
|
|
57
|
+
MAIN_REPO_ROOT="$REPO_ROOT"
|
|
58
|
+
|
|
59
|
+
set -a
|
|
60
|
+
# shellcheck source=/dev/null
|
|
61
|
+
source "$ENV_FILE"
|
|
62
|
+
set +a
|
|
63
|
+
|
|
64
|
+
WORK_DIR="$(resolve_agent_work_dir "$ENV_FILE")"
|
|
65
|
+
|
|
66
|
+
# ── Artifact directory (main repo root, not the worktree) ─────────────────────
|
|
67
|
+
ARTIFACT_DIR="$MAIN_REPO_ROOT/.har/artifacts/sast"
|
|
68
|
+
mkdir -p "$ARTIFACT_DIR"
|
|
69
|
+
|
|
70
|
+
SEMGREP_CONFIG="${HARNESS_SEMGREP_CONFIG:-auto}"
|
|
71
|
+
JSON_REPORT="$ARTIFACT_DIR/semgrep.json"
|
|
72
|
+
SARIF_REPORT="$ARTIFACT_DIR/semgrep.sarif"
|
|
73
|
+
SCAN_LOG="$ARTIFACT_DIR/scan.log"
|
|
74
|
+
|
|
75
|
+
# ── Run semgrep ───────────────────────────────────────────────────────────────
|
|
76
|
+
log "Scanning $WORK_DIR (config: $SEMGREP_CONFIG)..."
|
|
77
|
+
START=$(now_ms)
|
|
78
|
+
|
|
79
|
+
set +e
|
|
80
|
+
(
|
|
81
|
+
cd "$WORK_DIR" &&
|
|
82
|
+
semgrep scan \
|
|
83
|
+
--config "$SEMGREP_CONFIG" \
|
|
84
|
+
--error \
|
|
85
|
+
--exclude .har \
|
|
86
|
+
--json-output "$JSON_REPORT" \
|
|
87
|
+
--sarif-output "$SARIF_REPORT" \
|
|
88
|
+
${SCAN_PATHS[@]+"${SCAN_PATHS[@]}"}
|
|
89
|
+
) >"$SCAN_LOG" 2>&1
|
|
90
|
+
SEMGREP_EXIT=$?
|
|
91
|
+
set -e
|
|
92
|
+
|
|
93
|
+
END=$(now_ms)
|
|
94
|
+
TOTAL_MS=$(( END - START ))
|
|
95
|
+
|
|
96
|
+
# Surface the scan tail so failures are diagnosable without opening the log
|
|
97
|
+
tail -20 "$SCAN_LOG" | sed 's/^/ /' >&2
|
|
98
|
+
|
|
99
|
+
# semgrep exit codes: 0 = clean, 1 = findings (--error), >=2 = scan error
|
|
100
|
+
if [ "$SEMGREP_EXIT" = "0" ]; then
|
|
101
|
+
STATUS="pass"
|
|
102
|
+
log "✓ No blocking findings (${TOTAL_MS}ms)"
|
|
103
|
+
elif [ "$SEMGREP_EXIT" = "1" ]; then
|
|
104
|
+
STATUS="fail"
|
|
105
|
+
log "✗ Semgrep reported findings (${TOTAL_MS}ms) — see $JSON_REPORT"
|
|
106
|
+
else
|
|
107
|
+
STATUS="fail"
|
|
108
|
+
log "✗ Semgrep scan errored with exit code $SEMGREP_EXIT (${TOTAL_MS}ms) — see $SCAN_LOG"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# ── Output ────────────────────────────────────────────────────────────────────
|
|
112
|
+
node -e "
|
|
113
|
+
const fs = require('fs');
|
|
114
|
+
let findings = null;
|
|
115
|
+
let errors = null;
|
|
116
|
+
try {
|
|
117
|
+
const report = JSON.parse(fs.readFileSync('$JSON_REPORT', 'utf8'));
|
|
118
|
+
findings = report.results ? report.results.length : 0;
|
|
119
|
+
errors = report.errors ? report.errors.length : 0;
|
|
120
|
+
} catch { /* report missing when the scan itself errored */ }
|
|
121
|
+
const out = {
|
|
122
|
+
status: '$STATUS',
|
|
123
|
+
stageId: 'sast',
|
|
124
|
+
kind: 'test',
|
|
125
|
+
agent_id: $AGENT_ID,
|
|
126
|
+
total_ms: $TOTAL_MS,
|
|
127
|
+
exit_code: $SEMGREP_EXIT,
|
|
128
|
+
config: '$SEMGREP_CONFIG',
|
|
129
|
+
findings,
|
|
130
|
+
scan_errors: errors,
|
|
131
|
+
artifacts: [
|
|
132
|
+
{ path: '.har/artifacts/sast', kind: 'directory' },
|
|
133
|
+
{ path: '.har/artifacts/sast/semgrep.json', kind: 'report' }
|
|
134
|
+
]
|
|
135
|
+
};
|
|
136
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
137
|
+
"
|
|
138
|
+
|
|
139
|
+
if [ "$SEMGREP_EXIT" != "0" ]; then
|
|
140
|
+
exit "$SEMGREP_EXIT"
|
|
141
|
+
fi
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Semgrep plugin
|
|
2
|
+
|
|
3
|
+
Add a Semgrep SAST stage to your harness so agents catch security findings
|
|
4
|
+
before code lands.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
har env add-plugin semgrep
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## What this installs
|
|
11
|
+
|
|
12
|
+
| File | Purpose |
|
|
13
|
+
|------|---------|
|
|
14
|
+
| `.har/stages/sast.sh` | Stage runner — scans the session worktree with Semgrep |
|
|
15
|
+
| `.har/stages/SEMGREP.md` | Adaptation guide: rulesets, noise tuning, CI/Vanta story |
|
|
16
|
+
| `.github/workflows/semgrep.yml` | Optional CI workflow (`--skip-ci` to omit) — official `semgrep ci` recipe |
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Code change
|
|
22
|
+
│
|
|
23
|
+
launch slot
|
|
24
|
+
│
|
|
25
|
+
typecheck + unit-tests
|
|
26
|
+
│
|
|
27
|
+
sast ◄── semgrep scan over the worktree
|
|
28
|
+
│
|
|
29
|
+
pass/fail
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Full verify (`verify --full`) runs the scan after lint. Reports (JSON + SARIF)
|
|
33
|
+
land under `.har/artifacts/sast/`.
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- `semgrep` CLI: `pipx install semgrep` (recommended), `pip install semgrep`,
|
|
38
|
+
or `brew install semgrep`
|
|
39
|
+
- Network access for registry rulesets (`auto`, `p/ci`, …) — or commit local
|
|
40
|
+
rules and set `HARNESS_SEMGREP_CONFIG` to their path for offline scans
|
|
41
|
+
|
|
42
|
+
## Compliance (Vanta)
|
|
43
|
+
|
|
44
|
+
The local stage is shift-left only — compliance platforms never see it. The CI
|
|
45
|
+
workflow's `semgrep ci` run (with a `SEMGREP_APP_TOKEN` secret) publishes to the
|
|
46
|
+
Semgrep AppSec Platform, which Vanta ingests natively as vulnerability-management
|
|
47
|
+
evidence. See `.har/stages/SEMGREP.md` for the full story.
|
|
48
|
+
|
|
49
|
+
## Quick start after install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pipx install semgrep
|
|
53
|
+
./.har/launch.sh 1
|
|
54
|
+
./.har/stages/sast.sh 1
|
|
55
|
+
# reports: .har/artifacts/sast/semgrep.json (+ .sarif, scan.log)
|
|
56
|
+
./.har/verify.sh 1 --full
|
|
57
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "semgrep",
|
|
3
|
+
"stageId": "sast",
|
|
4
|
+
"verificationStages": [
|
|
5
|
+
"typecheck",
|
|
6
|
+
"unit-tests",
|
|
7
|
+
"lint",
|
|
8
|
+
"sast"
|
|
9
|
+
],
|
|
10
|
+
"stage": {
|
|
11
|
+
"id": "sast",
|
|
12
|
+
"kind": "test",
|
|
13
|
+
"description": "Static analysis (SAST) with Semgrep — scans the session worktree for security findings",
|
|
14
|
+
"script": "stages/sast.sh",
|
|
15
|
+
"requiresAgentId": true,
|
|
16
|
+
"artifacts": [
|
|
17
|
+
{
|
|
18
|
+
"path": ".har/artifacts/sast",
|
|
19
|
+
"kind": "directory",
|
|
20
|
+
"description": "Semgrep JSON + SARIF reports and scan log"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": ".har/artifacts/sast/semgrep.json",
|
|
24
|
+
"kind": "report"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
{ "src": ".har/stages/sast.sh", "dest": ".har/stages/sast.sh", "executable": true },
|
|
30
|
+
{ "src": ".har/stages/SEMGREP.md", "dest": ".har/stages/SEMGREP.md" }
|
|
31
|
+
],
|
|
32
|
+
"optionalFiles": [
|
|
33
|
+
{ "src": ".github/workflows/semgrep.yml", "dest": ".github/workflows/semgrep.yml", "skipFlag": "skipCi" }
|
|
34
|
+
],
|
|
35
|
+
"nextSteps": [
|
|
36
|
+
"pipx install semgrep # or: pip install semgrep / brew install semgrep",
|
|
37
|
+
"./.har/launch.sh 1",
|
|
38
|
+
"./.har/stages/sast.sh 1",
|
|
39
|
+
"Open .har/artifacts/sast/ for the JSON and SARIF reports",
|
|
40
|
+
"Read .har/stages/SEMGREP.md to pin rulesets (HARNESS_SEMGREP_CONFIG) or add local rules",
|
|
41
|
+
"CI: set the SEMGREP_APP_TOKEN secret so semgrep ci reports to the Semgrep AppSec Platform (compliance evidence, e.g. Vanta)"
|
|
42
|
+
],
|
|
43
|
+
"docsPath": ".har/stages/SEMGREP.md"
|
|
44
|
+
}
|