Skip to content

Commit bc3cd53

Browse files
committed
Harden install methods and audit tooling
1 parent 7251ea9 commit bc3cd53

16 files changed

Lines changed: 473 additions & 165 deletions

File tree

.github/scripts/check-container-pins.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ def compose_files() -> list[pathlib.Path]:
8080
return sorted(set(files))
8181
8282
83+
def helper_script_files() -> list[pathlib.Path]:
84+
roots = [repo / "scripts", repo / ".github" / "scripts", repo / "files" / "scripts"]
85+
files: list[pathlib.Path] = []
86+
for root in roots:
87+
if not root.exists():
88+
continue
89+
for path in root.rglob("*"):
90+
if path.is_file() and path.suffix in {".sh", ".bash", ".ps1"}:
91+
files.append(path)
92+
return sorted(files)
93+
94+
8395
errors = 0
8496
checked = 0
8597
@@ -120,6 +132,21 @@ for path in compose_files():
120132
print(f"ERROR: {rel(path)}:{line_no}: unpinned compose image {image_ref}")
121133
errors += 1
122134
135+
script_image_re = re.compile(
136+
r"(?P<ref>(?:docker\.io|quay\.io|ghcr\.io)/(?!secai-hub/secai_os\b)"
137+
r"[A-Za-z0-9][A-Za-z0-9._/-]*:[A-Za-z0-9._-]+(?:@[A-Za-z0-9:]+)?)"
138+
)
139+
for path in helper_script_files():
140+
for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
141+
if line.lstrip().startswith("#"):
142+
continue
143+
for match in script_image_re.finditer(line):
144+
image_ref = match.group("ref").strip('"\'')
145+
checked += 1
146+
if "@sha256:" not in image_ref:
147+
print(f"ERROR: {rel(path)}:{line_no}: unpinned helper image {image_ref}")
148+
errors += 1
149+
123150
if errors:
124151
print(f"FAIL: {errors} unpinned container image reference(s) found")
125152
sys.exit(1)

.github/workflows/ci.yml

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -259,34 +259,20 @@ jobs:
259259
with:
260260
go-version: "1.25"
261261

262-
- name: Install Syft (SBOM generator)
263-
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
264-
265262
- name: Install cosign (signing & attestation)
266263
run: |
267264
COSIGN_VERSION="v2.4.3"
268265
curl -sSfL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" \
269266
-o /usr/local/bin/cosign
270267
chmod +x /usr/local/bin/cosign
271268
272-
- name: Verify SBOM generation (Go services)
273-
run: |
274-
echo "=== SBOM generation verification ==="
275-
for svc in airlock registry tool-firewall gpu-integrity-watch mcp-firewall \
276-
policy-engine runtime-attestor integrity-monitor incident-recorder; do
277-
echo "--- ${svc} ---"
278-
syft dir:services/${svc} -o cyclonedx-json=/dev/null
279-
echo "OK: ${svc} SBOM generated"
280-
done
281-
282-
- name: Verify SBOM generation (Python services)
283-
run: |
284-
for svc in agent ui quarantine common diffusion-worker search-mediator; do
285-
if [ -d "services/${svc}" ]; then
286-
syft dir:services/${svc} -o cyclonedx-json=/dev/null
287-
echo "OK: ${svc} SBOM generated"
288-
fi
289-
done
269+
- name: Verify SBOM generation
270+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
271+
with:
272+
path: .
273+
format: cyclonedx-json
274+
output-file: /tmp/secai-os-sbom.cdx.json
275+
upload-artifact: false
290276

291277
- name: Verify cosign is functional
292278
run: |
@@ -514,7 +500,7 @@ jobs:
514500
python-version: "3.12"
515501

516502
- name: Install govulncheck
517-
run: go install golang.org/x/vuln/cmd/govulncheck@latest
503+
run: go install golang.org/x/vuln/cmd/govulncheck@v1.3.0
518504

519505
- name: Go vulnerability scan (enforced)
520506
run: |
@@ -827,7 +813,7 @@ jobs:
827813
- name: Install dependencies
828814
run: |
829815
pip install -r requirements-ci.txt
830-
go install golang.org/x/vuln/cmd/govulncheck@latest
816+
go install golang.org/x/vuln/cmd/govulncheck@v1.3.0
831817
832818
- name: Bandit (release-strict)
833819
run: |

.github/workflows/release.yml

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,15 @@ jobs:
9191
- name: Build (linux/amd64)
9292
working-directory: services/${{ matrix.service }}
9393
run: |
94+
mkdir -p ../../dist
9495
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
9596
go build -ldflags="-s -w -X main.version=${{ github.ref_name }}" \
9697
-o ../../dist/${{ matrix.service }}-linux-amd64 .
9798
9899
- name: Build (linux/arm64)
99100
working-directory: services/${{ matrix.service }}
100101
run: |
102+
mkdir -p ../../dist
101103
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
102104
go build -ldflags="-s -w -X main.version=${{ github.ref_name }}" \
103105
-o ../../dist/${{ matrix.service }}-linux-arm64 .
@@ -126,23 +128,56 @@ jobs:
126128
with:
127129
python-version: "3.12"
128130

129-
- name: Install Syft
130-
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
131+
- name: Prepare SBOM output directory
132+
run: mkdir -p dist
131133

132-
- name: Generate Python SBOMs
133-
run: |
134-
mkdir -p dist
135-
for svc in agent ui quarantine common; do
136-
if [ -d "services/${svc}" ]; then
137-
syft dir:services/${svc} -o cyclonedx-json=dist/${svc}-sbom.cdx.json
138-
fi
139-
done
140-
# Diffusion worker and search mediator
141-
for svc in diffusion-worker search-mediator; do
142-
if [ -d "services/${svc}" ]; then
143-
syft dir:services/${svc} -o cyclonedx-json=dist/${svc}-sbom.cdx.json
144-
fi
145-
done
134+
- name: Generate agent SBOM
135+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
136+
with:
137+
path: services/agent
138+
format: cyclonedx-json
139+
output-file: dist/agent-sbom.cdx.json
140+
upload-artifact: false
141+
142+
- name: Generate ui SBOM
143+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
144+
with:
145+
path: services/ui
146+
format: cyclonedx-json
147+
output-file: dist/ui-sbom.cdx.json
148+
upload-artifact: false
149+
150+
- name: Generate quarantine SBOM
151+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
152+
with:
153+
path: services/quarantine
154+
format: cyclonedx-json
155+
output-file: dist/quarantine-sbom.cdx.json
156+
upload-artifact: false
157+
158+
- name: Generate common SBOM
159+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
160+
with:
161+
path: services/common
162+
format: cyclonedx-json
163+
output-file: dist/common-sbom.cdx.json
164+
upload-artifact: false
165+
166+
- name: Generate diffusion-worker SBOM
167+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
168+
with:
169+
path: services/diffusion-worker
170+
format: cyclonedx-json
171+
output-file: dist/diffusion-worker-sbom.cdx.json
172+
upload-artifact: false
173+
174+
- name: Generate search-mediator SBOM
175+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
176+
with:
177+
path: services/search-mediator
178+
format: cyclonedx-json
179+
output-file: dist/search-mediator-sbom.cdx.json
180+
upload-artifact: false
146181

147182
- name: Upload artifacts
148183
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -158,7 +193,18 @@ jobs:
158193
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
159194

160195
- name: Build sandbox images required for VEX generation
161-
run: docker compose -f deploy/sandbox/compose.yaml --profile diffusion build ui agent search-mediator diffusion
196+
run: |
197+
for attempt in 1 2 3; do
198+
if docker compose -f deploy/sandbox/compose.yaml --profile diffusion build ui agent search-mediator diffusion; then
199+
exit 0
200+
fi
201+
if [ "$attempt" -eq 3 ]; then
202+
echo "Sandbox image build failed after ${attempt} attempts." >&2
203+
exit 1
204+
fi
205+
echo "Sandbox image build failed; retrying in $((attempt * 15)) seconds..."
206+
sleep $((attempt * 15))
207+
done
162208
163209
- name: Generate sandbox OpenVEX document
164210
run: |
@@ -301,7 +347,7 @@ jobs:
301347
- name: Build QCOW2
302348
run: |
303349
bash scripts/vm/build-qcow2.sh --ci \
304-
--image-ref "ghcr.io/secai-hub/secai_os:${{ github.ref_name }}"
350+
--image-ref "ghcr.io/secai-hub/secai_os:latest"
305351
306352
- name: Build OVA from QCOW2
307353
run: bash scripts/vm/build-ova.sh

.github/workflows/vm-boot-smoke.yml

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ name: VM Boot Smoke Test (Tier 2)
33
# Tier 2: Real VM boot test on self-hosted KVM runner.
44
# All checks run inside the guest via SSH (not host-side).
55
#
6-
# REQUIRES: self-hosted runner with KVM, QEMU, cloud-utils, and ssh-keygen.
6+
# REQUIRES: self-hosted runner with KVM, QEMU, libvirt/virt-install, cloud-utils,
7+
# and ssh-keygen.
78
# Gate: only runs when vars.HAS_KVM_RUNNER == 'true'.
89
#
910
# SECURITY: Never triggered by pull_request (fork safety for self-hosted runners).
@@ -110,9 +111,9 @@ jobs:
110111
- name: Wait for SSH readiness
111112
run: |
112113
echo "Waiting for guest SSH (up to 5 minutes)..."
113-
SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key"
114+
SSH_OPTS=(-o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key)
114115
for i in $(seq 1 60); do
115-
if ssh $SSH_OPTS -p 2222 root@localhost echo "SSH ready" 2>/dev/null; then
116+
if ssh "${SSH_OPTS[@]}" -p 2222 root@localhost echo "SSH ready" 2>/dev/null; then
116117
echo "Guest SSH is up after $((i * 5)) seconds"
117118
break
118119
fi
@@ -121,34 +122,34 @@ jobs:
121122
122123
- name: "Check: systemd system state"
123124
run: |
124-
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost"
125-
STATE=$($SSH "systemctl is-system-running" 2>/dev/null || true)
125+
SSH=(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost)
126+
STATE=$("${SSH[@]}" "systemctl is-system-running" 2>/dev/null || true)
126127
echo "System state: $STATE"
127128
if [ "$STATE" != "running" ] && [ "$STATE" != "degraded" ]; then
128129
echo "FAIL: expected running or degraded, got $STATE"
129-
$SSH "systemctl --failed" || true
130+
"${SSH[@]}" "systemctl --failed" || true
130131
exit 1
131132
fi
132133
133134
- name: "Check: auth endpoint responds"
134135
run: |
135-
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost"
136-
$SSH "curl -sf http://127.0.0.1:8480/api/auth/status" || {
136+
SSH=(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost)
137+
"${SSH[@]}" "curl -sf http://127.0.0.1:8480/api/auth/status" || {
137138
echo "FAIL: auth endpoint did not respond"
138139
exit 1
139140
}
140141
141142
- name: "Check: health endpoint"
142143
run: |
143-
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost"
144-
$SSH "curl -sf http://127.0.0.1:8480/health" || {
144+
SSH=(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost)
145+
"${SSH[@]}" "curl -sf http://127.0.0.1:8480/health" || {
145146
echo "FAIL: health endpoint did not respond"
146147
exit 1
147148
}
148149
149150
- name: "Check: disabled services stay inactive (offline_private profile)"
150151
run: |
151-
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost"
152+
SSH=(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost)
152153
DISABLED=(
153154
secure-ai-diffusion.service
154155
secure-ai-airlock.service
@@ -158,7 +159,7 @@ jobs:
158159
secure-ai-enable-diffusion.path
159160
)
160161
for svc in "${DISABLED[@]}"; do
161-
if $SSH "systemctl is-active --quiet $svc" 2>/dev/null; then
162+
if "${SSH[@]}" "systemctl is-active --quiet $svc" 2>/dev/null; then
162163
echo "FAIL: $svc is active (should be disabled in offline_private)"
163164
exit 1
164165
fi
@@ -167,31 +168,31 @@ jobs:
167168
168169
- name: "Check: default profile is offline_private"
169170
run: |
170-
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost"
171-
PROFILE=$($SSH "cat /var/lib/secure-ai/state/profile.json 2>/dev/null || echo '{}'")
171+
SSH=(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost)
172+
PROFILE=$("${SSH[@]}" "cat /var/lib/secure-ai/state/profile.json 2>/dev/null || echo '{}'")
172173
echo "Profile state: $PROFILE"
173174
# On first boot without wizard, profile.json may not exist yet — fallback is offline_private
174175
175176
- name: "Check: vault API responds"
176177
run: |
177-
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost"
178-
$SSH "curl -sf http://127.0.0.1:8480/api/vault/status" || {
178+
SSH=(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost)
179+
"${SSH[@]}" "curl -sf http://127.0.0.1:8480/api/vault/status" || {
179180
echo "FAIL: vault status endpoint did not respond"
180181
exit 1
181182
}
182183
183184
- name: "Check: quarantine directory exists"
184185
run: |
185-
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost"
186-
$SSH "test -d /var/lib/secure-ai/quarantine" || {
186+
SSH=(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost)
187+
"${SSH[@]}" "test -d /var/lib/secure-ai/quarantine" || {
187188
echo "FAIL: quarantine directory does not exist"
188189
exit 1
189190
}
190191
191192
- name: "Check: rpm-ostree deployment"
192193
run: |
193-
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost"
194-
$SSH "rpm-ostree status" || {
194+
SSH=(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/vm-smoke-key -p 2222 root@localhost)
195+
"${SSH[@]}" "rpm-ostree status" || {
195196
echo "FAIL: rpm-ostree status failed"
196197
exit 1
197198
}

0 commit comments

Comments
 (0)