Skip to content
Merged
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
94 changes: 87 additions & 7 deletions .ci-local.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# .ci-local.sh
# Runs a fixed set of checks that mirror CI intent, without CI event emulation.
# .ci-local.sh - runs a fixed set of checks that mirror CI workflow intent
# ------------------------------------------------------------------------------

set -Eeuo pipefail
Expand Down Expand Up @@ -230,7 +229,79 @@ trap 'on_err "$?" "$LINENO" "${BASH_COMMAND:-}" "${FUNCNAME[0]:-main}"' ERR

VERSIONS_FILE="${ROOT_DIR}/.ci-tool-versions.sh"
ensure_file "$VERSIONS_FILE"
source "$VERSIONS_FILE"

load_tool_versions() {
local file="$1"

local allowed_keys_re='^(TRIVY_VERSION|GOIMPORTS_VERSION|GOVULNCHECK_VERSION|GOLANGCI_LINT_VERSION|GO_MODULE|DEPENDENCY_CHECK_MAVEN_VERSION|DEPENDENCY_CHECK_FAIL_CVSS|PIP_AUDIT_VERSION|PIP_TOOLS_VERSION|PIP_MAX_VERSION)$'
local base_value_re='[A-Za-z0-9][A-Za-z0-9._+/:@-]*'

local unquoted_line_re="^([A-Z][A-Z0-9_]*)=(${base_value_re})$"
local quoted_line_re="^([A-Z][A-Z0-9_]*)=\"(${base_value_re})\"$"

local semverish_re='^v?[0-9]+(\.[0-9]+){0,3}([.-][0-9A-Za-z][0-9A-Za-z.-]*)?(\+[0-9A-Za-z][0-9A-Za-z.-]*)?$'
local numeric_version_re='^[0-9]+(\.[0-9]+){0,3}$'
local cvss_re='^(10(\.0)?|[0-9](\.[0-9])?)$'
local go_module_re='^[A-Za-z0-9.-]+(/[A-Za-z0-9._-]+)+$'

declare -A key_value_re=(
["TRIVY_VERSION"]="$semverish_re"
["GOIMPORTS_VERSION"]="$semverish_re"
["GOVULNCHECK_VERSION"]="$semverish_re"
["GOLANGCI_LINT_VERSION"]="$semverish_re"
["DEPENDENCY_CHECK_MAVEN_VERSION"]="$semverish_re"
["PIP_AUDIT_VERSION"]="$semverish_re"
["PIP_TOOLS_VERSION"]="$semverish_re"
["PIP_MAX_VERSION"]="$numeric_version_re"
["DEPENDENCY_CHECK_FAIL_CVSS"]="$cvss_re"
["GO_MODULE"]="$go_module_re"
)

_trim_ws() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}

while IFS= read -r raw || [[ -n "$raw" ]]; do
local line key value expected_re
line="${raw%$'\r'}"
line="$(_trim_ws "$line")"

[[ -z "$line" ]] && continue
[[ "$line" =~ ^# ]] && continue

key=""
value=""

if [[ "$line" =~ $unquoted_line_re ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"
elif [[ "$line" =~ $quoted_line_re ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"
else
die "invalid line in ${file}: must be KEY=value or KEY=\"value\"; no spaces; KEY=[A-Z][A-Z0-9_]*; value charset=${base_value_re}" 2
fi

if [[ ! "$key" =~ $allowed_keys_re ]]; then
die "disallowed key in ${file}: ${key} (allowed: ${allowed_keys_re})" 2
fi

expected_re="${key_value_re[$key]:-}"
[[ -n "$expected_re" ]] || die "internal config missing per-key validator for: ${key}" 2

if [[ ! "$value" =~ $expected_re ]]; then
die "invalid value for ${key} in ${file} (expected: ${expected_re}; got: ${value})" 2
fi

printf -v "$key" '%s' "$value"
export "$key"
done < "$file"
}

load_tool_versions "$VERSIONS_FILE"

# ------------------------------------------------------------------------------
# Net helpers
Expand Down Expand Up @@ -373,7 +444,6 @@ doctor_check_go() {
doctor_require_env GOIMPORTS_VERSION || issues=$((issues+1))
doctor_require_env GOVULNCHECK_VERSION || issues=$((issues+1))
doctor_require_env GOLANGCI_LINT_VERSION || issues=$((issues+1))
doctor_require_env GO_MODULE || issues=$((issues+1))

doctor_need_cmd go || issues=$((issues+1))
doctor_need_cmd curl || issues=$((issues+1))
Expand Down Expand Up @@ -414,6 +484,7 @@ doctor_check_python() {
doctor_require_file "${ROOT_DIR}/python-django/requirements.txt" || issues=$((issues+1))
doctor_require_file "${ROOT_DIR}/python-django/requirements.test.txt" || issues=$((issues+1))
doctor_require_file "${ROOT_DIR}/python-django/requirements.lock" || issues=$((issues+1))
doctor_require_file "${ROOT_DIR}/python-django/requirements.test.lock" || issues=$((issues+1))
doctor_require_dir "${ROOT_DIR}/python-django/app" || issues=$((issues+1))

doctor_require_env PIP_MAX_VERSION || issues=$((issues+1))
Expand All @@ -426,6 +497,7 @@ doctor_check_python() {
"${ROOT_DIR}/python-django/requirements.txt" \
"${ROOT_DIR}/python-django/requirements.test.txt" \
"${ROOT_DIR}/python-django/requirements.lock" \
"${ROOT_DIR}/python-django/requirements.test.lock" \
>/dev/null || issues=$((issues+1))

printf "%s\n" "$issues"
Expand Down Expand Up @@ -602,7 +674,6 @@ run_go() {
require_env GOIMPORTS_VERSION
require_env GOVULNCHECK_VERSION
require_env GOLANGCI_LINT_VERSION
require_env GO_MODULE

need_cmd go

Expand Down Expand Up @@ -635,7 +706,11 @@ run_go() {
done <<<"$go_dirs" | grep -q . && exit 1

while IFS= read -r d; do
goimports -l -local "${GO_MODULE}" "$d"
if [[ -n "${GO_MODULE:-}" ]]; then
goimports -l -local "${GO_MODULE}" "$d"
else
goimports -l "$d"
fi
done <<<"$go_dirs" | grep -q . && exit 1
fi

Expand Down Expand Up @@ -872,10 +947,15 @@ run_python() {
pip-compile --no-strip-extras --generate-hashes --output-file=requirements.lock requirements.txt
git diff --exit-code -- requirements.lock

say "Validate - requirements.test.lock matches requirements.test.txt"
test -f requirements.test.txt
pip-compile --no-strip-extras --generate-hashes --output-file=requirements.test.lock requirements.test.txt
git diff --exit-code -- requirements.test.lock

say "Install - runtime + test dependencies (locked)"
python -m pip install -U "pip<${PIP_MAX_VERSION}"
python -m pip install --require-hashes -r requirements.lock
python -m pip install -r requirements.test.txt
python -m pip install --require-hashes -r requirements.test.lock

need_cmd ruff
need_cmd mypy
Expand Down
2 changes: 1 addition & 1 deletion .ci-tool-versions.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# shellcheck shell=bash

# This file is sourced by CI/local scripts. Do not execute.
# This file is parsed by CI and may also be sourced; keep it KEY=value only.

# Go + Gin CI
GOIMPORTS_VERSION=v0.41.0
Expand Down
25 changes: 14 additions & 11 deletions .github/workflows/_dependency_check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,45 +23,46 @@ permissions:
jobs:
dependency_check:
name: Security - OWASP Dependency-Check
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 40
defaults:
run:
working-directory: ${{ inputs.working_directory }}
shell: bash
env:
DEPENDENCY_CHECK_MAVEN_VERSION: ""
DEPENDENCY_CHECK_FAIL_CVSS: ""
ODC_DATA_DIR: "${{ github.workspace }}/.cache/dependency-check"
ODC_CACHE_EPOCH: "unset"
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false

- name: Setup - load pinned tool versions
uses: ./.github/actions/load-tool-versions

- name: Validate - tool versions loaded
shell: bash
run: |
set -Eeuo pipefail
: "${DEPENDENCY_CHECK_MAVEN_VERSION:?DEPENDENCY_CHECK_MAVEN_VERSION is empty}"
: "${DEPENDENCY_CHECK_FAIL_CVSS:?DEPENDENCY_CHECK_FAIL_CVSS is empty}"

- name: Setup - JDK 25 (Temurin)
uses: actions/setup-java@v5
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: temurin
java-version: "25"
cache: maven

- name: Compute cache epoch (weekly)
shell: bash
run: echo "ODC_CACHE_EPOCH=$(date -u +%G-%V)" >> "$GITHUB_ENV"
run: |
set -Eeuo pipefail
echo "ODC_CACHE_EPOCH=$(date -u +%G-%V)" >> "$GITHUB_ENV"

- name: Cache - OWASP Dependency-Check data
uses: actions/cache@v5
uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
with:
path: ${{ env.ODC_DATA_DIR }}
key: dependency-check-${{ runner.os }}-${{ env.DEPENDENCY_CHECK_MAVEN_VERSION }}-${{ env.ODC_CACHE_EPOCH }}
Expand All @@ -70,10 +71,11 @@ jobs:

- name: Security - OWASP Dependency-Check scan
id: depcheck
continue-on-error: ${{ !inputs.gate }}
continue-on-error: true
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
run: |
set -Eeuo pipefail
mvn -B -ntp org.owasp:dependency-check-maven:${DEPENDENCY_CHECK_MAVEN_VERSION}:check \
-DfailBuildOnCVSS=${DEPENDENCY_CHECK_FAIL_CVSS} \
-Dformats=HTML,JSON \
Expand All @@ -82,7 +84,7 @@ jobs:

- name: Artifact - upload Dependency-Check reports
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ inputs.artifact_name }}
path: |
Expand All @@ -93,5 +95,6 @@ jobs:

- name: Gate - fail if Dependency-Check found issues
if: ${{ inputs.gate && steps.depcheck.outcome == 'failure' }}
shell: bash
run: exit 1
run: |
set -Eeuo pipefail
exit 1
29 changes: 15 additions & 14 deletions .github/workflows/_govulncheck.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,52 +20,53 @@ permissions:
jobs:
govulncheck:
name: Security - govulncheck
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
defaults:
run:
working-directory: ${{ inputs.working_directory }}
shell: bash
env:
GOVULNCHECK_VERSION: ""
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false

- name: Setup - load pinned tool versions
uses: ./.github/actions/load-tool-versions

- name: Validate - tool versions loaded
shell: bash
run: |
set -Eeuo pipefail
: "${GOVULNCHECK_VERSION:?GOVULNCHECK_VERSION is empty}"

- name: Setup - Go toolchain
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: ${{ inputs.working_directory }}/go.mod
cache-dependency-path: ${{ inputs.working_directory }}/go.sum
check-latest: false

- name: Cache - Go tools (govulncheck)
id: cache_go_tools
uses: actions/cache@v5
uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
with:
path: |
~/go/bin
key: go-tools-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-${{ env.GOVULNCHECK_VERSION }}

- name: Setup - install govulncheck (cache miss)
if: steps.cache_go_tools.outputs.cache-hit != 'true'
shell: bash
run: go install "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}"
run: |
set -Eeuo pipefail
go install "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}"

- name: Setup - add Go tools to PATH
shell: bash
run: |
set -Eeuo pipefail
GOBIN="$(go env GOBIN)"
if [[ -z "$GOBIN" ]]; then
GOPATH_FIRST="$(go env GOPATH)"
Expand All @@ -77,15 +78,14 @@ jobs:

- name: Security - govulncheck (with tests)
id: govuln
continue-on-error: ${{ !inputs.gate }}
shell: bash
continue-on-error: true
run: |
set -o pipefail
set -Eeuo pipefail
govulncheck -test ./... | tee govulncheck.txt

- name: Artifact - upload govulncheck report
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ inputs.artifact_name }}
path: ${{ inputs.working_directory }}/govulncheck.txt
Expand All @@ -94,5 +94,6 @@ jobs:

- name: Gate - fail if govulncheck found issues
if: ${{ inputs.gate && steps.govuln.outcome == 'failure' }}
shell: bash
run: exit 1
run: |
set -Eeuo pipefail
exit 1
Loading