feat: redis, fileserver, httpclient 런타임 시점 구현 추가
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Replace this template owner when forking the repository. CODEOWNERS is effective only when the
|
||||
# forge supports it and default-branch protection requires code-owner review.
|
||||
|
||||
/.trivyignore.yaml @DongHyeonka
|
||||
/.github/ @DongHyeonka
|
||||
/renovate.json @DongHyeonka
|
||||
/flaky-quarantine.yaml @DongHyeonka
|
||||
|
||||
# Public security and compatibility baselines require an explicit maintainer review.
|
||||
/docs/security/ @DongHyeonka
|
||||
/src/config/architecture/modules.json @DongHyeonka
|
||||
*.approved.json @DongHyeonka
|
||||
*.approved.txt @DongHyeonka
|
||||
@@ -0,0 +1,145 @@
|
||||
# Current repository CI controls. This file lists only mechanisms and jobs that exist in this
|
||||
# checkout. Build/release supply-chain, image, signing, provenance, SBOM, and tag-release jobs are
|
||||
# intentionally absent until their later bounded reconstruction.
|
||||
#
|
||||
# Fields:
|
||||
# release_blocking: true, false, or conditional
|
||||
# mechanism: gradle-custom-task, gradle-plugin-task, contract-test, workflow-job,
|
||||
# or delegated-pending
|
||||
# ref: task, plugin@task, repository-relative test path below src/, or workflow job id
|
||||
# workflow/job: canonical workflow and job that execute or represent the control
|
||||
# execution: check (through Gradle check), explicit (named in the job), or job
|
||||
gates:
|
||||
- id: format-lint
|
||||
release_blocking: true
|
||||
mechanism: gradle-plugin-task
|
||||
ref: com.diffplug.spotless@spotlessCheck
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: unit-and-contract-tests
|
||||
release_blocking: true
|
||||
mechanism: gradle-plugin-task
|
||||
ref: java@test
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: clean-architecture-dependencies
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyCleanArchitectureDependencies
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: environment-contract
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyEnvKeys
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: one-type-per-file
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyOneTypePerFile
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: readme-command-drift
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyReadmeCommands
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: trivy-suppression-governance
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyTrivyignore
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: quarantine-sunset
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyQuarantineSunset
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: public-path-snapshot
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyPublicPathSnapshot
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: explicit
|
||||
- id: dependency-locks
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyDependencyLocks
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: explicit
|
||||
- id: architecture-contract-test
|
||||
release_blocking: true
|
||||
mechanism: contract-test
|
||||
ref: app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: sample-off
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: sampleOffTest
|
||||
workflow: ci-quality-gates.yml
|
||||
job: sample-off
|
||||
execution: explicit
|
||||
- id: gate-matrix-lint
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: gate-matrix-lint
|
||||
workflow: ci-quality-gates.yml
|
||||
job: gate-matrix-lint
|
||||
execution: job
|
||||
- id: quality-release-gate
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: release-gate
|
||||
workflow: ci-quality-gates.yml
|
||||
job: release-gate
|
||||
execution: job
|
||||
- id: flaky-quarantine
|
||||
release_blocking: false
|
||||
mechanism: workflow-job
|
||||
ref: quarantine
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quarantine
|
||||
execution: job
|
||||
- id: dependency-review
|
||||
release_blocking: conditional
|
||||
mechanism: workflow-job
|
||||
ref: dependency-review
|
||||
workflow: dependency-vulnerability.yml
|
||||
job: dependency-review
|
||||
execution: job
|
||||
- id: dependency-submission
|
||||
release_blocking: false
|
||||
mechanism: workflow-job
|
||||
ref: dependency-submission
|
||||
workflow: dependency-vulnerability.yml
|
||||
job: dependency-submission
|
||||
execution: job
|
||||
- id: filesystem-vulnerability-scan
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: trivy-fs
|
||||
workflow: dependency-vulnerability.yml
|
||||
job: trivy-fs
|
||||
execution: job
|
||||
- id: documentation-links
|
||||
release_blocking: conditional
|
||||
mechanism: workflow-job
|
||||
ref: lychee
|
||||
workflow: link-check.yml
|
||||
job: lychee
|
||||
execution: job
|
||||
@@ -0,0 +1,18 @@
|
||||
# GitHub dependency-review configuration. The workflow guards this GitHub-only API so other forges
|
||||
# use the platform-neutral Trivy filesystem snapshot instead.
|
||||
fail-on-severity: high
|
||||
|
||||
fail-on-scopes:
|
||||
- runtime
|
||||
|
||||
# Template legal posture: deny strong and network copyleft for newly introduced dependencies.
|
||||
# Forks must have their legal/security owner review this organization-specific list.
|
||||
deny-licenses:
|
||||
- GPL-2.0-only
|
||||
- GPL-2.0-or-later
|
||||
- GPL-3.0-only
|
||||
- GPL-3.0-or-later
|
||||
- AGPL-3.0-only
|
||||
- AGPL-3.0-or-later
|
||||
|
||||
comment-summary-in-pr: never
|
||||
@@ -0,0 +1,92 @@
|
||||
# Dependency Vulnerability Policy
|
||||
|
||||
This policy is enforced by
|
||||
[`dependency-vulnerability.yml`](workflows/dependency-vulnerability.yml),
|
||||
[`dependency-review-config.yml`](dependency-review-config.yml),
|
||||
[`../.trivyignore.yaml`](../.trivyignore.yaml), `verifyTrivyignore`, CODEOWNERS, and
|
||||
[`../renovate.json`](../renovate.json).
|
||||
|
||||
## Execution and platform boundary
|
||||
|
||||
Canonical workflow files live under `.github/workflows`. The current origin is Gitea and
|
||||
server-side Actions is externally disabled; committing these controls does not enable or prove
|
||||
remote execution. An administrator must enable Actions, provide compatible runners, configure
|
||||
required checks, and require code-owner review separately.
|
||||
|
||||
External actions execute only by verified full commit SHA; inline comments retain the immutable
|
||||
release tag for review. GitHub dependency review publishes check output but never writes a PR
|
||||
summary comment, preserving the workflow-wide `contents: read` permission.
|
||||
|
||||
GitHub dependency review and dependency submission depend on GitHub.com APIs. They are guarded by
|
||||
`github.server_url == 'https://github.com'` and intentionally skip on Gitea. `trivy-fs` is the
|
||||
platform-neutral release-blocking snapshot and runs for pull requests, `main` pushes, daily
|
||||
schedule, and manual dispatch.
|
||||
|
||||
The later supply-chain slice owns image builds/scans, SBOM, signing, provenance, tag release, and
|
||||
retention. This policy does not claim those absent jobs.
|
||||
|
||||
## Severity and response
|
||||
|
||||
| Severity | CI posture | Target remediation |
|
||||
| --- | --- | --- |
|
||||
| KEV / Critical | block | 7 days or the CISA due date, whichever is sooner |
|
||||
| High | block | 30 days |
|
||||
| Medium | advisory | 90 days |
|
||||
| Low | advisory | best effort |
|
||||
|
||||
High/Critical is the template's release threshold. The exact threshold and remediation targets are
|
||||
team policy rather than an external mandate and should be reviewed when the template is adopted.
|
||||
GitHub dependency review applies the same High threshold to newly introduced runtime dependencies.
|
||||
|
||||
The Trivy JSON snapshot is also compared with the CISA Known Exploited Vulnerabilities catalog.
|
||||
Any intersection blocks regardless of Trivy severity. Before intersection, CI requires nonblank
|
||||
catalog metadata, a positive integral declared count, a non-empty vulnerability array whose length
|
||||
matches that count, and unique CVE-pattern `cveID` values. A missing, empty, malformed,
|
||||
count-inconsistent, duplicate, or unreachable KEV feed fails closed; configure the `KEV_FEED_URL`
|
||||
repository variable to an approved internal mirror when direct CISA access is unavailable.
|
||||
|
||||
Candidate extraction also validates Trivy's JSON first: the top level must be an object with a
|
||||
non-empty `Results` array; each result is an object whose `Vulnerabilities` value is null or an
|
||||
array; and every listed vulnerability has a nonblank string `VulnerabilityID`. Invalid scanner
|
||||
output fails closed instead of becoming an empty candidate set.
|
||||
|
||||
## Scanner and network requirements
|
||||
|
||||
Trivy scans the committed filesystem and Gradle lockfiles. High/Critical findings exit non-zero;
|
||||
Medium/Low findings are reported with exit zero. Scanner database or tool-download failures remain
|
||||
failures rather than silently producing an empty result.
|
||||
|
||||
Runners need HTTPS egress to:
|
||||
|
||||
- the configured Trivy binary source (GitHub Releases by default);
|
||||
- Trivy vulnerability databases (the scanner defaults, commonly OCI registries);
|
||||
- jq releases, unless `JQ_DOWNLOAD_BASE_URL` points to an internal mirror;
|
||||
- the CISA KEV feed, unless `KEV_FEED_URL` points to an internal mirror;
|
||||
- GitHub action sources when the forge does not mirror actions.
|
||||
|
||||
Closed networks must mirror the pinned Trivy/jq artifacts and checksums, set
|
||||
`TRIVY_DOWNLOAD_BASE_URL` and `JQ_DOWNLOAD_BASE_URL`, configure Trivy's documented database mirror
|
||||
environment, and set `KEV_FEED_URL`. Mirror availability and freshness are operational
|
||||
dependencies; stale mirrors can delay detection.
|
||||
|
||||
## Suppression governance
|
||||
|
||||
The only suppression source is repository-root `.trivyignore.yaml`. Every Trivy scan passes it
|
||||
explicitly with `--ignorefile .trivyignore.yaml`. Each future entry must contain an identifier, a
|
||||
non-empty rationale, and a future expiry no more than 90 days away. `verifyTrivyignore` validates
|
||||
the shape and expiry; CODEOWNERS plus branch protection controls who may approve the change.
|
||||
Neither control substitutes for the other.
|
||||
|
||||
Do not use an ad-hoc ignore file or an inline scanner bypass. An expired suppression is removed or
|
||||
renewed with fresh owner review and current evidence.
|
||||
|
||||
## Dependency update policy
|
||||
|
||||
Renovate is the checked-in security-update bot configuration. Patch/pin/digest security updates
|
||||
may auto-merge only after every configured required check succeeds; minor and major updates require
|
||||
human review. Dependency declarations and all 19 `gradle.lockfile` files must move together, and
|
||||
`verifyDependencyLocks` remains release-blocking.
|
||||
|
||||
Bot alerts are not a transitive-dependency backstop on every forge. Use a Gradle constraint or
|
||||
resolution rule for a vulnerable transitive dependency, refresh locks deliberately, and retain the
|
||||
full-snapshot Trivy scan.
|
||||
@@ -0,0 +1,18 @@
|
||||
## What changed and why
|
||||
|
||||
<!-- Summarize the behavior, architecture boundary, or policy being changed. -->
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] I ran the focused test for each changed leaf.
|
||||
- [ ] I ran `cd src && ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks`.
|
||||
- [ ] I did not add an unregistered production module dependency.
|
||||
- [ ] Dependency changes include refreshed `gradle.lockfile` files and a strict-lock verification.
|
||||
- [ ] Trivy suppressions include an owner-reviewed reason and an expiry within 90 days.
|
||||
- [ ] Any quarantined test is registered in `flaky-quarantine.yaml` with a reason, issue, and
|
||||
`quarantined_since` date.
|
||||
- [ ] I documented checks that could not run and their remaining risk.
|
||||
|
||||
## Compatibility and operations
|
||||
|
||||
<!-- Note public API/config changes, migration needs, rollout/rollback impact, and external blockers. -->
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly JQ_VERSION='1.8.1'
|
||||
readonly JQ_SHA256_AMD64='020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d'
|
||||
readonly JQ_SHA256_ARM64='6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4'
|
||||
readonly DOWNLOAD_BASE_URL="${JQ_DOWNLOAD_BASE_URL:-https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}}"
|
||||
|
||||
: "${RUNNER_TEMP:?RUNNER_TEMP must be set by the CI runner}"
|
||||
: "${GITHUB_PATH:?GITHUB_PATH must be set by the CI runner}"
|
||||
|
||||
architecture="${RUNNER_ARCH:-$(uname -m)}"
|
||||
case "${architecture}" in
|
||||
X64 | x86_64 | amd64)
|
||||
asset='jq-linux-amd64'
|
||||
expected_sha256="${JQ_SHA256_AMD64}"
|
||||
;;
|
||||
ARM64 | aarch64 | arm64)
|
||||
asset='jq-linux-arm64'
|
||||
expected_sha256="${JQ_SHA256_ARM64}"
|
||||
;;
|
||||
*)
|
||||
printf '::error::install-jq: unsupported runner architecture: %s\n' "${architecture}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
install_dir="${RUNNER_TEMP}/jq-${JQ_VERSION}/bin"
|
||||
destination="${install_dir}/jq"
|
||||
mkdir -p "${install_dir}"
|
||||
|
||||
temporary="$(mktemp "${RUNNER_TEMP}/jq-${JQ_VERSION}.XXXXXX")"
|
||||
trap 'rm -f "${temporary}"' EXIT
|
||||
|
||||
curl --fail --show-error --silent --location --retry 3 \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
"${DOWNLOAD_BASE_URL}/${asset}" \
|
||||
--output "${temporary}"
|
||||
printf '%s %s\n' "${expected_sha256}" "${temporary}" | sha256sum -c -
|
||||
chmod 0755 "${temporary}"
|
||||
mv "${temporary}" "${destination}"
|
||||
trap - EXIT
|
||||
|
||||
printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}"
|
||||
installed_version="$("${destination}" --version)"
|
||||
if [[ "${installed_version}" != "jq-${JQ_VERSION}" ]]; then
|
||||
printf '::error::install-jq: expected jq-%s, got %s\n' "${JQ_VERSION}" "${installed_version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'install-jq: %s installed under RUNNER_TEMP\n' "${installed_version}"
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
readonly REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel)"
|
||||
readonly EXPECTED_SCRIPT_DIR="$(cd -- "${REPO_ROOT}/.github/scripts" && pwd -P)"
|
||||
readonly MATRIX="${REPO_ROOT}/.github/ci-gate-matrix.yml"
|
||||
readonly EXPECTED_GATE_COUNT=19
|
||||
|
||||
if [[ "${SCRIPT_DIR}" != "${EXPECTED_SCRIPT_DIR}" ]]; then
|
||||
printf '::error::gate-matrix-lint: script resolved outside the repository .github/scripts directory\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${MATRIX}" ]]; then
|
||||
printf '::error::gate-matrix-lint: missing %s\n' "${MATRIX}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
records="$(
|
||||
awk '
|
||||
function flush() {
|
||||
if (id != "") {
|
||||
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", id, blocking, mechanism, ref, workflow, job, execution
|
||||
}
|
||||
}
|
||||
/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/ {
|
||||
flush()
|
||||
id=$0
|
||||
sub(/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/, "", id)
|
||||
blocking=mechanism=ref=workflow=job=execution=""
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+release_blocking:[[:space:]]*/ {
|
||||
blocking=$0
|
||||
sub(/^[[:space:]]+release_blocking:[[:space:]]*/, "", blocking)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+mechanism:[[:space:]]*/ {
|
||||
mechanism=$0
|
||||
sub(/^[[:space:]]+mechanism:[[:space:]]*/, "", mechanism)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+ref:[[:space:]]*/ {
|
||||
ref=$0
|
||||
sub(/^[[:space:]]+ref:[[:space:]]*/, "", ref)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+workflow:[[:space:]]*/ {
|
||||
workflow=$0
|
||||
sub(/^[[:space:]]+workflow:[[:space:]]*/, "", workflow)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+job:[[:space:]]*/ {
|
||||
job=$0
|
||||
sub(/^[[:space:]]+job:[[:space:]]*/, "", job)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+execution:[[:space:]]*/ {
|
||||
execution=$0
|
||||
sub(/^[[:space:]]+execution:[[:space:]]*/, "", execution)
|
||||
next
|
||||
}
|
||||
END { flush() }
|
||||
' "${MATRIX}"
|
||||
)"
|
||||
|
||||
declare -A seen_ids=()
|
||||
declare -a failures=()
|
||||
total=0
|
||||
verified=0
|
||||
delegated=0
|
||||
|
||||
job_body() {
|
||||
local workflow_file="$1"
|
||||
local job_id="$2"
|
||||
awk -v target="${job_id}" '
|
||||
$0 ~ "^ " target ":[[:space:]]*$" { inside=1; print; next }
|
||||
inside && $0 ~ "^ [A-Za-z0-9_-]+:[[:space:]]*$" { exit }
|
||||
inside { print }
|
||||
' "${workflow_file}"
|
||||
}
|
||||
|
||||
while IFS=$'\t' read -r id blocking mechanism ref workflow job execution; do
|
||||
[[ -z "${id}" ]] && continue
|
||||
total=$((total + 1))
|
||||
|
||||
if [[ -n "${seen_ids[${id}]:-}" ]]; then
|
||||
failures+=("duplicate gate id '${id}'")
|
||||
fi
|
||||
seen_ids["${id}"]=1
|
||||
|
||||
if [[ -z "${blocking}" || -z "${mechanism}" || -z "${ref}" || -z "${workflow}" \
|
||||
|| -z "${job}" || -z "${execution}" ]]; then
|
||||
failures+=("gate '${id}' has an empty required field")
|
||||
continue
|
||||
fi
|
||||
if [[ ! "${blocking}" =~ ^(true|false|conditional)$ ]]; then
|
||||
failures+=("gate '${id}' has invalid release_blocking '${blocking}'")
|
||||
fi
|
||||
if [[ ! "${workflow}" =~ ^[A-Za-z0-9._-]+\.ya?ml$ || ! "${job}" =~ ^[A-Za-z0-9_-]+$ ]]; then
|
||||
failures+=("gate '${id}' has an unsafe workflow or job identifier")
|
||||
continue
|
||||
fi
|
||||
|
||||
workflow_file="${REPO_ROOT}/.github/workflows/${workflow}"
|
||||
if [[ ! -f "${workflow_file}" ]]; then
|
||||
failures+=("gate '${id}' references missing workflow '.github/workflows/${workflow}'")
|
||||
continue
|
||||
fi
|
||||
if ! grep -Eqs -- "^[[:space:]]{2}${job}:[[:space:]]*$" "${workflow_file}"; then
|
||||
failures+=("gate '${id}' references missing job '${job}' in '${workflow}'")
|
||||
continue
|
||||
fi
|
||||
|
||||
case "${mechanism}" in
|
||||
gradle-custom-task)
|
||||
if ! grep -RqsE -- "tasks\\.register\\(['\"]${ref}['\"]" "${REPO_ROOT}/src" \
|
||||
--include='build.gradle'; then
|
||||
failures+=("gate '${id}' references unregistered Gradle task '${ref}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
gradle-plugin-task)
|
||||
plugin="${ref%@*}"
|
||||
task="${ref#*@}"
|
||||
if [[ "${plugin}" == "${ref}" || -z "${task}" ]]; then
|
||||
failures+=("gate '${id}' must use plugin@task for gradle-plugin-task")
|
||||
continue
|
||||
fi
|
||||
if ! grep -RqsE -- "(id|apply plugin:)[[:space:]]+['\"]${plugin}['\"]" "${REPO_ROOT}/src" \
|
||||
--include='build.gradle'; then
|
||||
failures+=("gate '${id}' references unapplied Gradle plugin '${plugin}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
contract-test)
|
||||
if [[ "${ref}" == /* || "${ref}" == *".."* || ! -f "${REPO_ROOT}/src/${ref}" ]]; then
|
||||
failures+=("gate '${id}' references missing or unsafe contract test 'src/${ref}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
workflow-job)
|
||||
if [[ "${ref}" != "${job}" ]]; then
|
||||
failures+=("gate '${id}' workflow-job ref '${ref}' must equal job '${job}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
delegated-pending)
|
||||
delegated=$((delegated + 1))
|
||||
printf "gate '%s': explicitly delegated-pending\n" "${id}"
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
failures+=("gate '${id}' has unknown mechanism '${mechanism}'")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${execution}" in
|
||||
check)
|
||||
if ! job_body "${workflow_file}" "${job}" | grep -Eqs -- '\./gradlew[[:space:]]+check([[:space:]]|$)'; then
|
||||
failures+=("gate '${id}' expects Gradle check in job '${job}'")
|
||||
continue
|
||||
fi
|
||||
if [[ "${mechanism}" == "gradle-custom-task" ]] \
|
||||
&& ! grep -RqsE -- "dependsOn.*named\\(['\"]${ref}['\"]\\)" "${REPO_ROOT}/src" \
|
||||
--include='build.gradle'; then
|
||||
failures+=("gate '${id}' task '${ref}' exists but is not wired into Gradle check")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
explicit)
|
||||
if ! job_body "${workflow_file}" "${job}" | grep -Fqs -- "${ref}"; then
|
||||
failures+=("gate '${id}' task '${ref}' is not explicit in job '${job}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
job)
|
||||
;;
|
||||
*)
|
||||
failures+=("gate '${id}' has unknown execution '${execution}'")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
verified=$((verified + 1))
|
||||
done <<< "${records}"
|
||||
|
||||
if (( total != EXPECTED_GATE_COUNT )); then
|
||||
failures+=("matrix has ${total} gates; expected ${EXPECTED_GATE_COUNT}")
|
||||
fi
|
||||
|
||||
printf 'gate-matrix-lint: %d gates, %d verified, %d delegated-pending\n' \
|
||||
"${total}" "${verified}" "${delegated}"
|
||||
if (( ${#failures[@]} > 0 )); then
|
||||
printf '::error::gate-matrix-lint: %d drift(s) found\n' "${#failures[@]}" >&2
|
||||
for failure in "${failures[@]}"; do
|
||||
printf ' - %s\n' "${failure}" >&2
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
printf 'gate-matrix-lint: OK\n'
|
||||
@@ -0,0 +1,113 @@
|
||||
name: ci-quality-gates
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: ["main"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TESTCONTAINERS_REUSE_ENABLE: "false"
|
||||
|
||||
jobs:
|
||||
quality-gates:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Require the committed public-path security baseline
|
||||
run: |
|
||||
set -euo pipefail
|
||||
readonly snapshot='docs/security/public-paths-snapshot.txt'
|
||||
if [[ ! -s "${snapshot}" ]]; then
|
||||
echo "::error::${snapshot} is missing or empty. CI must not let verifyPublicPathSnapshot create its own first-run baseline."
|
||||
exit 1
|
||||
fi
|
||||
if ! git ls-files --error-unmatch "${snapshot}" >/dev/null 2>&1; then
|
||||
echo "::error::${snapshot} exists locally but is not committed."
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Check quality, public paths, and dependency locks
|
||||
working-directory: src
|
||||
run: ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --no-daemon --stacktrace
|
||||
|
||||
sample-off:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Verify the application without the sample fixture
|
||||
working-directory: src
|
||||
run: ./gradlew :app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies --no-daemon --stacktrace
|
||||
|
||||
gate-matrix-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Verify the gate matrix against the repository
|
||||
run: bash .github/scripts/verify-gate-matrix.sh
|
||||
|
||||
# Advisory only. Quarantine expiry/drift remains blocking through verifyQuarantineSunset in check.
|
||||
quarantine:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run quarantined tests as an advisory signal
|
||||
working-directory: src
|
||||
run: ./gradlew quarantineTest --no-daemon
|
||||
|
||||
release-gate:
|
||||
needs:
|
||||
- quality-gates
|
||||
- sample-off
|
||||
- gate-matrix-lint
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Require every current blocking job to succeed
|
||||
env:
|
||||
QUALITY_RESULT: ${{ needs.quality-gates.result }}
|
||||
SAMPLE_OFF_RESULT: ${{ needs.sample-off.result }}
|
||||
MATRIX_RESULT: ${{ needs.gate-matrix-lint.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for result in "${QUALITY_RESULT}" "${SAMPLE_OFF_RESULT}" "${MATRIX_RESULT}"; do
|
||||
if [[ "${result}" != "success" ]]; then
|
||||
echo "::error::release-gate: required job result was ${result}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "release-gate: all current blocking quality jobs succeeded."
|
||||
@@ -0,0 +1,185 @@
|
||||
name: dependency-vulnerability
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "0 6 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# The compare API exists only on GitHub.com. Trivy remains the full-snapshot backstop elsewhere.
|
||||
dependency-review:
|
||||
if: github.event_name == 'pull_request' && github.server_url == 'https://github.com'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Review newly introduced dependencies
|
||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # actions/dependency-review-action@v4.8.0
|
||||
with:
|
||||
config-file: ./.github/dependency-review-config.yml
|
||||
|
||||
# The submission API is also GitHub.com-only and is not required for the platform-neutral scan.
|
||||
dependency-submission:
|
||||
if: github.event_name == 'push' && github.server_url == 'https://github.com'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Submit the resolved Gradle dependency graph
|
||||
uses: gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # gradle/actions@v4.4.4
|
||||
with:
|
||||
build-root-directory: src
|
||||
|
||||
trivy-fs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Install pinned Trivy under RUNNER_TEMP
|
||||
env:
|
||||
TRIVY_DOWNLOAD_BASE_URL: ${{ vars.TRIVY_DOWNLOAD_BASE_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
readonly TRIVY_VERSION='0.71.2'
|
||||
readonly TRIVY_SHA256_AMD64='0510e71e2fd39bf863856d499c8dc19feb4e7336546394c502a8f5cc7ab27460'
|
||||
readonly TRIVY_SHA256_ARM64='fe1c7106e15a5365d485b098a8c338f91e3b7ba71cb0e4963b98a3a098763cfc'
|
||||
readonly DOWNLOAD_BASE_URL="${TRIVY_DOWNLOAD_BASE_URL:-https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}}"
|
||||
case "${RUNNER_ARCH:-X64}" in
|
||||
X64)
|
||||
asset_arch='64bit'
|
||||
expected_sha256="${TRIVY_SHA256_AMD64}"
|
||||
;;
|
||||
ARM64)
|
||||
asset_arch='ARM64'
|
||||
expected_sha256="${TRIVY_SHA256_ARM64}"
|
||||
;;
|
||||
*)
|
||||
echo "::error::unsupported runner architecture: ${RUNNER_ARCH:-unknown}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
install_dir="${RUNNER_TEMP}/trivy-${TRIVY_VERSION}"
|
||||
archive="${RUNNER_TEMP}/trivy-${TRIVY_VERSION}.tar.gz"
|
||||
mkdir -p "${install_dir}"
|
||||
curl --fail --show-error --silent --location --retry 3 \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
"${DOWNLOAD_BASE_URL}/trivy_${TRIVY_VERSION}_Linux-${asset_arch}.tar.gz" \
|
||||
--output "${archive}"
|
||||
printf '%s %s\n' "${expected_sha256}" "${archive}" | sha256sum -c -
|
||||
tar -xzf "${archive}" -C "${install_dir}" trivy
|
||||
chmod 0755 "${install_dir}/trivy"
|
||||
printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}"
|
||||
- name: Install checksum-pinned jq
|
||||
env:
|
||||
JQ_DOWNLOAD_BASE_URL: ${{ vars.JQ_DOWNLOAD_BASE_URL }}
|
||||
run: bash .github/scripts/install-jq.sh
|
||||
|
||||
- name: Block High and Critical vulnerabilities
|
||||
run: |
|
||||
trivy fs \
|
||||
--scanners vuln,license \
|
||||
--severity CRITICAL,HIGH \
|
||||
--exit-code 1 \
|
||||
--ignorefile .trivyignore.yaml \
|
||||
.
|
||||
|
||||
- name: Report Medium and Low vulnerabilities
|
||||
run: |
|
||||
trivy fs \
|
||||
--scanners vuln,license \
|
||||
--severity MEDIUM,LOW \
|
||||
--exit-code 0 \
|
||||
--ignorefile .trivyignore.yaml \
|
||||
.
|
||||
|
||||
- name: Produce the governed all-severity KEV input
|
||||
run: |
|
||||
trivy fs \
|
||||
--scanners vuln \
|
||||
--severity CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN \
|
||||
--exit-code 0 \
|
||||
--ignorefile .trivyignore.yaml \
|
||||
--format json \
|
||||
--output trivy-kev.json \
|
||||
.
|
||||
|
||||
- name: Fail closed on any CISA KEV match
|
||||
env:
|
||||
CONFIGURED_KEV_FEED_URL: ${{ vars.KEV_FEED_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
readonly DEFAULT_KEV_FEED_URL='https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json'
|
||||
readonly KEV_FEED_URL="${CONFIGURED_KEV_FEED_URL:-${DEFAULT_KEV_FEED_URL}}"
|
||||
if ! curl --fail --show-error --silent --location --retry 3 \
|
||||
--proto '=https' --tlsv1.2 "${KEV_FEED_URL}" --output kev.json; then
|
||||
echo "::error::KEV feed unavailable; configure KEV_FEED_URL to an approved internal mirror"
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '
|
||||
(.catalogVersion | type == "string" and test("\\S"))
|
||||
and (.dateReleased | type == "string" and test("\\S"))
|
||||
and (.count | type == "number")
|
||||
and (.count > 0)
|
||||
and (.count == (.count | floor))
|
||||
and (.vulnerabilities | type == "array")
|
||||
and ((.vulnerabilities | length) > 0)
|
||||
and (.count == (.vulnerabilities | length))
|
||||
and (all(
|
||||
.vulnerabilities[];
|
||||
(.cveID | type == "string" and test("^CVE-[0-9]{4}-[0-9]{4,}$"))
|
||||
))
|
||||
and (([.vulnerabilities[].cveID] | unique | length) == .count)
|
||||
' kev.json >/dev/null; then
|
||||
echo "::error::KEV feed is malformed, empty, count-inconsistent, or contains invalid/duplicate cveID values"
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '
|
||||
(type == "object")
|
||||
and (.Results | type == "array")
|
||||
and ((.Results | length) > 0)
|
||||
and (all(.Results[]; type == "object"))
|
||||
and (all(
|
||||
.Results[];
|
||||
(.Vulnerabilities == null) or (.Vulnerabilities | type == "array")
|
||||
))
|
||||
and (all(
|
||||
.Results[];
|
||||
all(
|
||||
.Vulnerabilities[]?;
|
||||
(type == "object")
|
||||
and (.VulnerabilityID | type == "string" and test("\\S"))
|
||||
)
|
||||
))
|
||||
' trivy-kev.json >/dev/null; then
|
||||
echo "::error::Trivy KEV input is malformed, empty, or contains an invalid VulnerabilityID"
|
||||
exit 1
|
||||
fi
|
||||
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID | select(type == "string")] | unique[]?' \
|
||||
trivy-kev.json | sort -u > found-cves.txt
|
||||
jq -r '.vulnerabilities[]?.cveID | select(type == "string")' \
|
||||
kev.json | sort -u > kev-cves.txt
|
||||
hits="$(comm -12 found-cves.txt kev-cves.txt || true)"
|
||||
if [[ -n "${hits}" ]]; then
|
||||
echo "::error::CISA KEV-listed vulnerability found regardless of CVSS:"
|
||||
printf '%s\n' "${hits}"
|
||||
exit 1
|
||||
fi
|
||||
echo "KEV cross-check: no catalog match."
|
||||
@@ -0,0 +1,43 @@
|
||||
name: link-check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "README.md"
|
||||
- "src/README.md"
|
||||
- "docs/**/*.md"
|
||||
- ".github/**/*.md"
|
||||
- ".github/workflows/link-check.yml"
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "README.md"
|
||||
- "src/README.md"
|
||||
- "docs/**/*.md"
|
||||
- ".github/**/*.md"
|
||||
- ".github/workflows/link-check.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lychee:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Check committed documentation links
|
||||
uses: lycheeverse/lychee-action@7cd0af4c74a61395d455af97419279d86aafaede # lycheeverse/lychee-action@v2.0.2
|
||||
with:
|
||||
args: >-
|
||||
--no-progress
|
||||
--root-dir .
|
||||
README.md
|
||||
src/README.md
|
||||
'docs/**/*.md'
|
||||
'.github/**/*.md'
|
||||
fail: true
|
||||
Reference in New Issue
Block a user