Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 139 additions & 27 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ on:
# Run security scan weekly to reduce cost (Mon 05:20 UTC)
- cron: '20 5 * * 1'
workflow_dispatch:
inputs:
mode:
description: Security execution scope (focused mode is diagnostic evidence, not a full security pass)
required: true
default: all
type: choice
options:
- all
- container-security
expected_head:
description: Exact 40-character commit SHA required by focused container-security mode
required: false
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
Expand All @@ -28,6 +41,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
run_security: ${{ steps.lbl.outputs.run_security }}
run_container_security: ${{ steps.lbl.outputs.run_container_security }}
steps:
- uses: actions/github-script@v7
id: lbl
Expand All @@ -36,8 +50,20 @@ jobs:
const isPR = context.eventName === 'pull_request';
const isFork = isPR && context.payload.pull_request?.head?.repo?.fork;
const labels = (context.payload.pull_request?.labels || []).map(l=>l.name);
const runSecurity = !isPR || (!isFork && labels.includes('run-security'));
const focusedContainer = context.eventName === 'workflow_dispatch'
&& context.payload.inputs?.mode === 'container-security';
if (focusedContainer) {
const expectedHead = context.payload.inputs?.expected_head;
if (!/^[a-f0-9]{40}$/.test(expectedHead || '') || expectedHead !== context.sha) {
core.setFailed('Focused Container Security requires expected_head to match github.sha exactly.');
return;
}
}
const normalSecurityAllowed = !isPR || (!isFork && labels.includes('run-security'));
const runSecurity = normalSecurityAllowed && !focusedContainer;
const runContainerSecurity = normalSecurityAllowed;
core.setOutput('run_security', String(runSecurity));
core.setOutput('run_container_security', String(runContainerSecurity));
core.setOutput('is_fork', String(Boolean(isFork)));
security-scan:
needs: gate
Expand Down Expand Up @@ -362,12 +388,18 @@ jobs:
container-security:
name: Container Security
runs-on: ubuntu-latest
timeout-minutes: 45
needs: gate
if: ${{ needs.gate.outputs.run_security == 'true' && github.event_name != 'pull_request' }}
if: ${{ needs.gate.outputs.run_container_security == 'true' && github.event_name != 'pull_request' }}
permissions:
contents: read
security-events: write
actions: read
env:
CONTAINER_RUNTIME: runc
MINIMAL_RUNTIME_IMAGE: docker.io/library/alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
TRIVY_IMAGE: ghcr.io/aquasecurity/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f
RUNTIME_REPORT: artifacts/container-security/container-runtime-diagnostic.json

steps:
- name: Checkout code
Expand All @@ -379,52 +411,132 @@ jobs:
id: container_manifest
uses: ./.github/actions/detect-container-manifest

- name: Install Podman
if: steps.container_manifest.outputs.skip != 'true'
- name: Verify runner container runtime
id: runtime_preflight
run: |
sudo apt-get update
sudo apt-get install -y podman
node scripts/ci/container-runtime-preflight.mjs preflight \
--runtime "${CONTAINER_RUNTIME}" \
--minimal-image "${MINIMAL_RUNTIME_IMAGE}" \
--trivy-image "${TRIVY_IMAGE}" \
--repository-dockerfile "${{ steps.container_manifest.outputs.dockerfile }}" \
--report "${RUNTIME_REPORT}"

- name: Require container manifest
run: |
outcome=success
if [ "${{ steps.container_manifest.outputs.skip }}" = "true" ]; then
outcome=failure
fi
node scripts/ci/container-runtime-preflight.mjs record-stage \
--report "${RUNTIME_REPORT}" \
--stage manifest-detect \
--outcome "${outcome}"

- name: Build container image
if: steps.container_manifest.outputs.skip != 'true'
run: podman build -f "${{ steps.container_manifest.outputs.dockerfile }}" -t ae-framework:latest .
run: |
node scripts/ci/container-runtime-preflight.mjs run-stage \
--report "${RUNTIME_REPORT}" \
--stage repository-build \
--timeout-ms 1800000 \
-- "${{ steps.runtime_preflight.outputs.podman_path }}" \
--runtime "${{ steps.runtime_preflight.outputs.runtime_path }}" \
build \
--file "${{ steps.container_manifest.outputs.dockerfile }}" \
--tag ae-framework:latest \
.

- name: Verify production image user
if: steps.container_manifest.outputs.skip != 'true'
run: |
node scripts/ci/container-runtime-preflight.mjs run-stage \
--report "${RUNTIME_REPORT}" \
--stage image-user \
--expect-stdout nextjs \
-- "${{ steps.runtime_preflight.outputs.podman_path }}" \
image inspect --format '{{.Config.User}}' ae-framework:latest

- name: Export image archive
if: steps.container_manifest.outputs.skip != 'true'
run: podman save ae-framework:latest -o ae-framework.tar
run: |
node scripts/ci/container-runtime-preflight.mjs run-stage \
--report "${RUNTIME_REPORT}" \
--stage archive-export \
--timeout-ms 600000 \
-- "${{ steps.runtime_preflight.outputs.podman_path }}" \
save \
--output artifacts/container-security/ae-framework.tar \
ae-framework:latest

node scripts/ci/container-runtime-preflight.mjs validate-artifact \
--report "${RUNTIME_REPORT}" \
--stage archive-export \
--path artifacts/container-security/ae-framework.tar \
--kind archive

- name: Run Trivy vulnerability scanner
if: steps.container_manifest.outputs.skip != 'true'
run: |
TRIVY_IMAGE="ghcr.io/aquasecurity/trivy:latest"
podman pull "${TRIVY_IMAGE}"
podman run --rm \
--security-opt label=disable \
-v "$PWD:/workspace" \
-w /workspace \
"${TRIVY_IMAGE}" \
image --input /workspace/ae-framework.tar --format sarif --output /workspace/trivy-results.sarif
node scripts/ci/container-runtime-preflight.mjs run-stage \
--report "${RUNTIME_REPORT}" \
--stage trivy-pull \
--timeout-ms 600000 \
-- "${{ steps.runtime_preflight.outputs.podman_path }}" \
pull "${TRIVY_IMAGE}"

node scripts/ci/container-runtime-preflight.mjs run-stage \
--report "${RUNTIME_REPORT}" \
--stage trivy-scan \
--timeout-ms 1200000 \
-- "${{ steps.runtime_preflight.outputs.podman_path }}" \
--runtime "${{ steps.runtime_preflight.outputs.runtime_path }}" \
run --rm \
--security-opt label=disable \
-v "${GITHUB_WORKSPACE}/artifacts/container-security:/workspace" \
-w /workspace \
"${TRIVY_IMAGE}" \
image --input /workspace/ae-framework.tar --format sarif --output /workspace/trivy-results.sarif

node scripts/ci/container-runtime-preflight.mjs validate-artifact \
--report "${RUNTIME_REPORT}" \
--stage sarif-validate \
--path artifacts/container-security/trivy-results.sarif \
--kind sarif

- name: Upload Trivy scan results
id: sarif_upload
uses: github/codeql-action/upload-sarif@v3
if: ${{ always() && steps.container_manifest.outputs.skip != 'true' && hashFiles('trivy-results.sarif') != '' }}
if: ${{ success() && steps.container_manifest.outputs.skip != 'true' && hashFiles('artifacts/container-security/trivy-results.sarif') != '' }}
with:
sarif_file: 'trivy-results.sarif'
sarif_file: 'artifacts/container-security/trivy-results.sarif'

- name: Record SARIF upload outcome
if: ${{ always() && steps.container_manifest.outputs.skip != 'true' && steps.runtime_preflight.outcome == 'success' }}
run: |
node scripts/ci/container-runtime-preflight.mjs record-stage \
--report "${RUNTIME_REPORT}" \
--stage sarif-upload \
--outcome "${{ steps.sarif_upload.outcome }}"

- name: Note missing Trivy SARIF
if: ${{ always() && steps.container_manifest.outputs.skip != 'true' && hashFiles('trivy-results.sarif') == '' }}
- name: Finalize container security diagnostic
if: ${{ success() && steps.container_manifest.outputs.skip != 'true' }}
run: |
printf "%s\n" "::warning::trivy-results.sarif was not generated; skipping SARIF upload"
{
printf "%s\n" "### Container Security"
printf "%s\n" "- trivy-results.sarif: missing"
printf "%s\n" "- upload-sarif: skipped"
} >> "$GITHUB_STEP_SUMMARY"
node scripts/ci/container-runtime-preflight.mjs finalize \
--report "${RUNTIME_REPORT}"

- name: Upload container runtime diagnostic
uses: actions/upload-artifact@v4
if: ${{ always() && hashFiles('artifacts/container-security/container-runtime-diagnostic.json') != '' }}
with:
name: container-runtime-diagnostic
path: artifacts/container-security/container-runtime-diagnostic.json
if-no-files-found: error
retention-days: 14

security-notification:
name: Security Notification
runs-on: ubuntu-latest
needs: [security-scan, dependency-audit, secrets-scan, codeql-analysis]
needs: [security-scan, dependency-audit, secrets-scan, codeql-analysis, container-security]
if: failure() && github.event_name == 'schedule'

steps:
Expand Down
4 changes: 2 additions & 2 deletions docs/ci/ci-troubleshooting-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Purpose: provide a short, deterministic path to diagnose common CI failures and
- Notification decisions for `automation-observability-weekly` are persisted in `weekly-alert-summary.json`. If no notification was sent, inspect `suppressed` and `suppressedReason`.
- SLO / MTTR thresholds are defined in `docs/ci/automation-slo-mttr.md` and evaluated from the weekly `automation-observability-weekly` artifact.
- `Security Analysis / Secrets Scanning` is skipped when `GITLEAKS_LICENSE` is not configured. Add the repository secret `GITLEAKS_LICENSE` to enable the scan.
- `Container Security` skips SARIF upload and emits a warning when `trivy-results.sarif` is missing. Check the build failure log before triaging the upload step.
- `Container Security` fails closed when the archive or `trivy-results.sarif` is missing, malformed, oversized, or crosses the non-symlink artifact boundary. Inspect the uploaded `container-runtime-diagnostic` artifact for the stable build/export/scan/SARIF classification before triaging the upload step. Only `pipelineComplete=true` after all 12 reviewed checks pass is complete Container Security evidence; a failure artifact remains useful with `pipelineComplete=false`.

### 6. Symptom-to-runbook map

Expand Down Expand Up @@ -223,7 +223,7 @@ gh workflow run "Codex Autopilot Lane" --ref <HEAD_BRANCH> -f pr_number=12345 -f
- `automation-observability-weekly` の通知判定は `weekly-alert-summary.json` に保存される。通知が来ない場合は `suppressed` と `suppressedReason` を確認する。
- SLO / MTTR の判定基準は `docs/ci/automation-slo-mttr.md` にあり、週次 `automation-observability-weekly` artifact で評価される。
- `Security Analysis / Secrets Scanning` は `GITLEAKS_LICENSE` 未設定時に skip される。scan を有効化する場合は repository secret `GITLEAKS_LICENSE` を設定する。
- `Container Security` `trivy-results.sarif` 未生成時に SARIF upload を skip して warning を出す。upload を疑う前に build 失敗ログを確認する
- `Container Security` はimage archiveまたは`trivy-results.sarif`が欠落/malformed/oversized、またはnon-symlink artifact boundaryを外れる場合にfail closedとなる。upload stepの前に、upload済み`container-runtime-diagnostic` artifactでbuild/export/scan/SARIFのstable classificationを確認する。complete Evidenceは12個のreview済みcheckがすべてpassした後の`pipelineComplete=true`だけであり、failure artifactは`pipelineComplete=false`のまま診断に使用する

### 6. 症状 → runbook 対応表

Expand Down
33 changes: 29 additions & 4 deletions docs/infra/container-runtime.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
docRole: ssot
lastVerified: '2026-03-12'
lastVerified: '2026-07-31'
owner: infra-ops
verificationCommand: pnpm -s run check:doc-consistency
---
Expand Down Expand Up @@ -104,10 +104,35 @@ pnpm pipelines:mutation:quick

GitHub Actions でも同じ Podman ベースのパイプラインが動作します。ローカルで `pnpm pipelines:full` を実行して Verify Lite/Pact/API fuzz/Mutation Quick がすべて緑化することを確認してから PR を作成してください。Podman で取得したレポート (`reports/`, `artifacts/hermetic-reports/`) はそのまま CI の成果物構成と一致します。

### CI 共有ランナー向け手順
### GitHub-hosted Container Security runtime

GitHub Actions 上で Podman を利用する場合は、rootless Podman を有効にした専用ランナーを用意する必要があります。パッケージの導入、`loginctl enable-linger`、`podman.socket` の常駐化など詳細な手順は以下を参照してください
`Security Analysis` の `Container Security` job は、GitHub-hosted Ubuntu image が提供する reviewed container-tool bundleを使用します。job内で `apt-get install podman` を重ねると、runner bundleのPodman/conmon/OCI runtimeとUbuntu packageの旧tupleが混在するため、実行直前のpackage追加は行いません

- [Podman 共有ランナー構築ガイド](./podman-shared-runner.md)
jobは `scripts/ci/container-runtime-preflight.mjs` を通して次をfail closedで確認します。

- runner image/kernel/architectureと、`apt-cache policy`によるPodman/Buildah/conmon/crun/runc inventory
- system path配下のregular executableと、parse可能なversion
- OCI runtime `features`応答
- digest-pinned minimal imageによる`podman run --rm`とminimal Containerfile `RUN true`
- explicit runtimeを適用した`podman info --debug`のeffective runtime
- repository image build、non-root `nextjs` user、archive export、Trivy scan、SARIF validation/upload

既定runtimeはrunner bundleの`runc`です。`/usr/local/bin/runc`を優先し、存在・version・direct smoke・minimal run/build・Podman effective runtimeがすべて一致した場合だけrepository buildへ進みます。runtimeの自動fallbackや、build/scan/SARIF failureのwarning変換は行いません。

bounded evidenceは`artifacts/container-security/container-runtime-diagnostic.json`へ`container-runtime-diagnostic/v1`として生成されます。`graphRoot`のprivate absolute pathやenvironment全量は保存せず、user/system storageの分類だけを保持します。classificationは`runtime-missing`、`runtime-version-incompatible`、`runtime-selection-invalid`、各build/export/scan/SARIF failureなどのclosed vocabularyです。check resultは`pass`/`fail`/`not-run`ごとにexit code、duration、detail、classificationの組み合わせを閉じ、timeout、malformed output、missing outputを別failureとして保持します。runtime candidateも同じstatus semanticsを使用し、unavailable candidateや未実行candidateをpassへ変換しません。

要求runtimeに成功candidateがない場合も、top-levelの`direct-runtime-smoke`と`minimal-run`は選択したfailure candidateの実測resultから導出します。direct smokeを通過してminimal runで失敗したcandidateを、direct smokeで失敗したcandidateより先に評価し、同じ進行段階では成功選択と同じく`/usr/local`を`/usr`より優先します。timeout、nonzero exit、duration、detailをgeneric failureへ置き換えず、candidateの`selected`は全件false、`selectedRuntime`はnull、`pipelineComplete`はfalseのままです。要求runtime candidate自体が存在しない場合は`runtime-missing`とし、direct/minimal checkを未実行として記録して架空のexit codeやdurationを生成しません。

preflightの成功時点では`pipelineComplete=false`です。`manifest-detect -> repository-build -> image-user -> archive-export -> trivy-pull -> trivy-scan -> sarif-validate -> sarif-upload`の順に前提を検査し、12個すべてのreview済みcheckが一意にpassした後、`finalize`コマンドだけが`pipelineComplete=true`へ更新します。したがってruntime-readyな中間reportやfailure reportは診断用途には使用できますが、complete Container Security Evidenceとしては扱いません。

archive、SARIF、diagnostic reportのread boundaryはrepository-relative pathだけを受け取り、`.`/`..`/backslash/absolute pathと、finalを含む全path componentのsymlinkを拒否します。これはread前のbounded validationであり、同時filesystem mutationに対するatomic openを主張しません。structured artifactはread前にsizeを検査し、diagnostic JSONは256 KiB、Trivy SARIFは16 MiBを上限とします。上限超過時はtruncateせずfail closedです。上限変更は実際のartifact size evidence、memory bound、workflow timeoutを同じreviewで確認してください。

新規に制御するminimal smoke imageとTrivy imageはtagとdigestを併記します。更新時は、upstream release/registry digestを確認したreview済みPRでworkflow、fixture、runbookを同時更新し、同一exact headの独立した`workflow_dispatch`を2回成功させてください。repositoryの`node:22-alpine` base imageは今回のruntime selection contractとは別管理であり、無関係な大量pin変更は行いません。

`workflow_dispatch`の`mode=container-security`は、Container Security runtime pathだけを再現するfocused diagnosticです。実行時は`expected_head`へ40桁のcommit SHAを渡し、`github.sha`との完全一致をgateで検証します。通常の`push`/`schedule`では選択できず、Container Securityをsilent skipしません。focused runの成功はbuild/export/Trivy/SARIF pathのEvidenceであり、Dependency Audit、CodeQL、SBOMを含むfull Security Analysis成功へ昇格しません。full security acceptanceには`mode=all`または通常trusted eventの各lane成功が別途必要です。

### CI 共有ランナー向け手順

長時間の共有runnerやself-hosted laneでPodmanを利用する場合は、rootless Podmanを有効にした専用ランナーを用意します。パッケージの導入、`loginctl enable-linger`、`podman.socket` の常駐化など詳細な手順は以下を参照してください。GitHub-hosted `Container Security` jobは前節のbounded preflightを使用し、このself-hosted setupを暗黙の前提にしません。

- [Podman 共有ランナー構築ガイド](./podman-shared-runner.md)
Loading
Loading