trigger: test with fresh Gitea token
This commit is contained in:
149
.woodpecker/harden.yml
Normal file
149
.woodpecker/harden.yml
Normal file
@@ -0,0 +1,149 @@
|
||||
# ── Hardening pipeline ────────────────────────────────────────────
|
||||
# Rebuilds all images with apt-get/apk upgrade, scans for vulns,
|
||||
# and auto-closes Gitea vuln issues when scans are clean.
|
||||
# Trigger: manual or weekly cron.
|
||||
|
||||
when:
|
||||
- event: [manual, cron]
|
||||
|
||||
variables:
|
||||
- &buildx_image woodpeckerci/plugin-docker-buildx:5-insecure
|
||||
- &buildx_base
|
||||
buildkit_config: |
|
||||
[registry."gitea.homelab.fhirworx.io"]
|
||||
http = true
|
||||
registry: gitea.homelab.fhirworx.io
|
||||
username:
|
||||
from_secret: registry_user
|
||||
password:
|
||||
from_secret: registry_pass
|
||||
- &trivy_scan
|
||||
image: aquasec/trivy:latest
|
||||
- &diag_env
|
||||
UV_PYTHON_PREFERENCE: only-system
|
||||
UV_LINK_MODE: copy
|
||||
UV_PROJECT_ENVIRONMENT: .venv
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
|
||||
steps:
|
||||
# ── Build hardened images ──────────────────────────────────
|
||||
- name: build-notebooks
|
||||
image: *buildx_image
|
||||
settings:
|
||||
<<: *buildx_base
|
||||
repo: gitea.homelab.fhirworx.io/homelab/notebooks
|
||||
dockerfile: notebooks/Dockerfile
|
||||
context: notebooks/
|
||||
tags: [hardened, latest]
|
||||
pull_image: true
|
||||
no_cache: true
|
||||
|
||||
- name: build-zotero
|
||||
image: *buildx_image
|
||||
settings:
|
||||
<<: *buildx_base
|
||||
repo: gitea.homelab.fhirworx.io/homelab/zotero
|
||||
dockerfile: zotero/Dockerfile
|
||||
context: zotero/
|
||||
tags: [hardened, latest]
|
||||
pull_image: true
|
||||
no_cache: true
|
||||
|
||||
- name: build-docs
|
||||
image: *buildx_image
|
||||
settings:
|
||||
<<: *buildx_base
|
||||
repo: gitea.homelab.fhirworx.io/homelab/docs
|
||||
dockerfile: docs/Dockerfile
|
||||
context: .
|
||||
tags: [hardened, latest]
|
||||
pull_image: true
|
||||
no_cache: true
|
||||
|
||||
- name: build-api
|
||||
image: *buildx_image
|
||||
settings:
|
||||
<<: *buildx_base
|
||||
repo: gitea.homelab.fhirworx.io/homelab/api
|
||||
dockerfile: api/Dockerfile
|
||||
context: .
|
||||
tags: [hardened, latest]
|
||||
pull_image: true
|
||||
no_cache: true
|
||||
|
||||
- name: build-mc
|
||||
image: *buildx_image
|
||||
settings:
|
||||
<<: *buildx_base
|
||||
repo: gitea.homelab.fhirworx.io/homelab/mc
|
||||
dockerfile: rustfs/Dockerfile.mc
|
||||
context: rustfs/
|
||||
tags: [hardened, latest]
|
||||
pull_image: true
|
||||
no_cache: true
|
||||
|
||||
# ── Scan hardened images ───────────────────────────────────
|
||||
- name: scan-notebooks
|
||||
<<: *trivy_scan
|
||||
commands:
|
||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
||||
--exit-code 0 --format json -o notebooks-scan.json
|
||||
"gitea.homelab.fhirworx.io/homelab/notebooks:hardened"
|
||||
depends_on: [build-notebooks]
|
||||
|
||||
- name: scan-zotero
|
||||
<<: *trivy_scan
|
||||
commands:
|
||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
||||
--exit-code 0 --format json -o zotero-scan.json
|
||||
"gitea.homelab.fhirworx.io/homelab/zotero:hardened"
|
||||
depends_on: [build-zotero]
|
||||
|
||||
- name: scan-docs
|
||||
<<: *trivy_scan
|
||||
commands:
|
||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
||||
--exit-code 0 --format json -o docs-scan.json
|
||||
"gitea.homelab.fhirworx.io/homelab/docs:hardened"
|
||||
depends_on: [build-docs]
|
||||
|
||||
- name: scan-api
|
||||
<<: *trivy_scan
|
||||
commands:
|
||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
||||
--exit-code 0 --format json -o api-scan.json
|
||||
"gitea.homelab.fhirworx.io/homelab/api:hardened"
|
||||
depends_on: [build-api]
|
||||
|
||||
# ── Close resolved vuln issues OR file new ones ────────────
|
||||
- name: close-or-report-vulns
|
||||
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
environment:
|
||||
<<: *diag_env
|
||||
commands:
|
||||
- uv sync --no-dev
|
||||
- |
|
||||
for f in notebooks-scan.json zotero-scan.json docs-scan.json api-scan.json; do
|
||||
if [ -f "$f" ]; then
|
||||
uv run python -m api.diag.vuln --close "$f" || \
|
||||
uv run python -m api.diag.vuln "$f" || true
|
||||
fi
|
||||
done
|
||||
depends_on:
|
||||
- scan-notebooks
|
||||
- scan-zotero
|
||||
- scan-docs
|
||||
- scan-api
|
||||
|
||||
# ── Failure reporter ─────────────────────────────────────────
|
||||
- name: report-failure
|
||||
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
failure: ignore
|
||||
environment:
|
||||
<<: *diag_env
|
||||
commands:
|
||||
- uv sync --no-dev
|
||||
- uv run python -m api.diag
|
||||
when:
|
||||
- status: [failure]
|
||||
@@ -3,6 +3,9 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Patch base image CVEs
|
||||
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy project files for install
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
COPY src/ src/
|
||||
@@ -18,6 +21,9 @@ COPY stack.toml ./
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
||||
|
||||
CMD ["uv", "run", "--no-sync", "uvicorn", "api.server:app", \
|
||||
"--host", "0.0.0.0", "--port", "8000", \
|
||||
"--workers", "1", "--log-level", "info"]
|
||||
|
||||
@@ -44,7 +44,12 @@ RUN pnpm build
|
||||
# hadolint ignore=DL3006
|
||||
FROM nginx:alpine
|
||||
|
||||
RUN apk upgrade --no-cache
|
||||
|
||||
COPY --from=build /docs/build /usr/share/nginx/html
|
||||
COPY docs/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD wget -qO /dev/null http://localhost:80/ || exit 1
|
||||
|
||||
@@ -13,7 +13,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
|
||||
# System dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
git \
|
||||
@@ -50,6 +50,9 @@ COPY --chown=${USER_UID}:${USER_GID} .marimo-config/loch.css /home/${USERNAME}/.
|
||||
|
||||
EXPOSE 2718
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD curl -sf http://localhost:2718/health || exit 1
|
||||
|
||||
CMD ["uv", "run", "--project", "/home/kert/workspace", \
|
||||
"marimo", "edit", \
|
||||
"--host", "0.0.0.0", "--port", "2718", "--headless", "--no-token", \
|
||||
|
||||
@@ -182,28 +182,107 @@ def file_vuln_issue(
|
||||
return None
|
||||
|
||||
|
||||
def close_vuln_issue(
|
||||
scan_path: str | Path,
|
||||
*,
|
||||
owner: str = "homelab",
|
||||
repo: str = "stack",
|
||||
) -> bool:
|
||||
"""Close the matching vuln issue if scan is clean (no fixable HIGH/CRITICAL).
|
||||
|
||||
Returns True if an issue was closed, False otherwise.
|
||||
"""
|
||||
path = Path(scan_path)
|
||||
if not path.exists():
|
||||
log.warning("Scan file not found: %s", path)
|
||||
return False
|
||||
|
||||
data = json.loads(path.read_text())
|
||||
report = parse_trivy_json(data)
|
||||
short_image = report.image.rsplit("/", 1)[-1].split(":")[0]
|
||||
|
||||
token = os.environ.get("GITEA_TOKEN", "")
|
||||
if not token:
|
||||
log.error("No GITEA_TOKEN — cannot close issues")
|
||||
return False
|
||||
|
||||
client = GiteaClient(token)
|
||||
|
||||
if report.vulns:
|
||||
log.info(
|
||||
"%s still has %d fixable vulns — not closing",
|
||||
short_image,
|
||||
len(report.vulns),
|
||||
)
|
||||
client.close()
|
||||
return False
|
||||
|
||||
# Find open vuln issue for this image
|
||||
try:
|
||||
existing = client.get(
|
||||
f"/repos/{owner}/{repo}/issues",
|
||||
params={"state": "open", "type": "issues", "limit": 50},
|
||||
).json()
|
||||
for issue in existing:
|
||||
if f"vuln: {short_image}" in issue.get("title", ""):
|
||||
# Close with comment
|
||||
client.post(
|
||||
f"/repos/{owner}/{repo}/issues/{issue['number']}/comments",
|
||||
json={
|
||||
"body": "Scan clean — no fixable HIGH/CRITICAL vulnerabilities remaining.\n\nClosed by hardening pipeline."
|
||||
},
|
||||
)
|
||||
client.patch(
|
||||
f"/repos/{owner}/{repo}/issues/{issue['number']}",
|
||||
json={"state": "closed"},
|
||||
)
|
||||
log.info("Closed vuln issue #%s for %s", issue["number"], short_image)
|
||||
client.close()
|
||||
return True
|
||||
except Exception:
|
||||
log.exception("Failed to close vuln issue for %s", short_image)
|
||||
|
||||
log.info("No open vuln issue found for %s", short_image)
|
||||
client.close()
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""CLI entry point: parse scan JSON files and file issues."""
|
||||
"""CLI entry point: parse scan JSON files and file/close issues."""
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <scan.json> [scan2.json ...]", file=sys.stderr)
|
||||
close_mode = "--close" in sys.argv
|
||||
paths = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
|
||||
if not paths:
|
||||
print(
|
||||
f"Usage: {sys.argv[0]} [--close] <scan.json> [scan2.json ...]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
pipeline = os.environ.get("CI_PIPELINE_NUMBER", "")
|
||||
label_ids = [11] # quality label
|
||||
|
||||
filed = 0
|
||||
for path in sys.argv[1:]:
|
||||
result = file_vuln_issue(
|
||||
path,
|
||||
pipeline=pipeline,
|
||||
label_ids=label_ids,
|
||||
)
|
||||
if result:
|
||||
filed += 1
|
||||
closed = 0
|
||||
for path in paths:
|
||||
if close_mode:
|
||||
if close_vuln_issue(path):
|
||||
closed += 1
|
||||
else:
|
||||
result = file_vuln_issue(
|
||||
path,
|
||||
pipeline=pipeline,
|
||||
label_ids=label_ids,
|
||||
)
|
||||
if result:
|
||||
filed += 1
|
||||
|
||||
log.info("Filed %d vuln issue(s)", filed)
|
||||
if close_mode:
|
||||
log.info("Closed %d vuln issue(s)", closed)
|
||||
else:
|
||||
log.info("Filed %d vuln issue(s)", filed)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from api.diag.vuln import (
|
||||
Vuln,
|
||||
build_vuln_issue_body,
|
||||
build_vuln_issue_title,
|
||||
close_vuln_issue,
|
||||
file_vuln_issue,
|
||||
parse_trivy_json,
|
||||
)
|
||||
@@ -252,3 +253,40 @@ class TestFileVulnIssue:
|
||||
issue = call_args[2]
|
||||
assert "vuln:" in issue["title"]
|
||||
assert issue["labels"] == [11]
|
||||
|
||||
|
||||
class TestCloseVulnIssue:
|
||||
def test_skips_if_vulns_remain(self, tmp_path: Path) -> None:
|
||||
scan = tmp_path / "scan.json"
|
||||
scan.write_text(json.dumps(SAMPLE_TRIVY))
|
||||
with patch.dict("os.environ", {"GITEA_TOKEN": "tok"}, clear=False):
|
||||
result = close_vuln_issue(scan)
|
||||
assert result is False
|
||||
|
||||
def test_closes_matching_issue(self, tmp_path: Path) -> None:
|
||||
scan = tmp_path / "scan.json"
|
||||
scan.write_text(json.dumps(EMPTY_TRIVY))
|
||||
mock_client = MagicMock()
|
||||
mock_client.get.return_value.json.return_value = [
|
||||
{"number": 114, "title": "vuln: empty-image — old issue"}
|
||||
]
|
||||
with (
|
||||
patch.dict("os.environ", {"GITEA_TOKEN": "tok"}, clear=False),
|
||||
patch("api.diag.vuln.GiteaClient", return_value=mock_client),
|
||||
):
|
||||
result = close_vuln_issue(scan)
|
||||
assert result is True
|
||||
mock_client.patch.assert_called_once()
|
||||
mock_client.post.assert_called_once()
|
||||
|
||||
def test_no_issue_to_close(self, tmp_path: Path) -> None:
|
||||
scan = tmp_path / "scan.json"
|
||||
scan.write_text(json.dumps(EMPTY_TRIVY))
|
||||
mock_client = MagicMock()
|
||||
mock_client.get.return_value.json.return_value = []
|
||||
with (
|
||||
patch.dict("os.environ", {"GITEA_TOKEN": "tok"}, clear=False),
|
||||
patch("api.diag.vuln.GiteaClient", return_value=mock_client),
|
||||
):
|
||||
result = close_vuln_issue(scan)
|
||||
assert result is False
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM ghcr.io/selkies-project/nvidia-egl-desktop:latest
|
||||
USER root
|
||||
|
||||
# Install Zotero
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& curl -sL https://raw.githubusercontent.com/retorquere/zotero-deb/master/install.sh | bash \
|
||||
|
||||
Reference in New Issue
Block a user