외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.
삭제
- .github/ci-gate-matrix.yml(1,025줄) + verify-gate-matrix.sh(568줄):
Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제
- verify-gradle-wrapper.sh(799줄): workflow 바이트 해시 잠금.
wrapper 검증은 gradle/actions/wrapper-validation(full SHA 핀)에 위임
- DeveloperExperienceContractTest 등의 CI YAML mutation 테스트:
애플리케이션 test suite가 GitHub Actions YAML 파서를 검증하던 계층 역전
- 문서 drift 파서: verifyReadmeCommands, verifyRunbookReferences,
verifyDocumentedLeafCount, verifyTestSourceSetRegistry
- 빈 레지스트리를 지키던 커스텀 YAML 파서: verifyTrivyignore,
verifyQuarantineSunset, flaky-quarantine.yaml
- verifyConfigurationPropertiesProcessor, verifyOneTypePerFile:
각각 ca.spring-config convention과 Checkstyle OneTopLevelClass가 대체
- 정상 입력으로도 성공할 수 없던 messaging always-fail task
- ModuleRegistry의 JSON 필드 집합 정확 일치, sample-portfolio negative guard
이동
- java/quality/spring 공통 설정을 configure(subprojects) 블록에서
ca.java-conventions / ca.quality-conventions / ca.java-library /
ca.spring-library convention plugin으로
- 아키텍처 검증을 ca.architecture로, JPA·messaging qualification을
gradle/qualification/ 아래로, verifyEnvKeys를 :app-bootstrap 소유로
완화
- Git revision은 releaseCheck·아카이브 생성에서만 요구. 일반 빌드는 SNAPSHOT
- SpotBugs/FindSecBugs는 로컬 check에서 빼고 qualityCheck 레인으로
task 계층
- leaf check는 그 leaf만. architectureCheck / qualityCheck /
configContractCheck / integrationCheck / ci / releaseCheck로 이름 분리
CI
- _reusable-gradle.yml 신규. checkout + wrapper validation + JDK/캐시 공통화
- fileserver-release.yml -> fileserver-certification.yml (CD가 아니라 certification)
- GitHub Actions = CI + artifact, Argo CD = CD 경계를 docs/ci-cd/boundary.md로 고정
순증감 +3,274 / -7,483.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
181 lines
7.3 KiB
YAML
181 lines
7.3 KiB
YAML
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: ./.github/actions/setup-gradle-java
|
|
- 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
|
|
# No `|| true`. comm exits non-zero only when it cannot read or order its inputs, and
|
|
# swallowing that would have turned an unreadable CVE list into an empty intersection and
|
|
# printed "no catalog match" — a KEV cross-check that passes because it never ran.
|
|
hits="$(comm -12 found-cves.txt kev-cves.txt)"
|
|
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."
|