diff --git a/.github/scripts/container_scan.py b/.github/scripts/container_scan.py new file mode 100644 index 00000000..7cd2cdac --- /dev/null +++ b/.github/scripts/container_scan.py @@ -0,0 +1,243 @@ +import subprocess +import os +import sys +import logging +import json +import argparse +from parse_sarif import evaluate + + +GREEN = '\033[92m' +RED = '\033[91m' +RESET = '\033[0m' +BOLD = '\033[1m' +YELLOW = '\033[93m' + +logging.basicConfig( + level=logging.INFO, + format='%(message)s' # Clean format to prevent double-timestamps in CI logs +) +logger = logging.getLogger("container-scan-orchestrator") + +# --- Configurable values +IMAGE_NAME = os.getenv("IMAGE_NAME", "platform-backend:local") + +# --- SCA / CVE (Trivy + OSV) --- +TRIVY_IGNOREFILE = os.getenv("TRIVY_IGNOREFILE", ".github/scripts/suppress_trivy.yaml") +OSV_IGNOREFILE = os.getenv("OSV_IGNOREFILE", ".github/scripts/suppress_osv_scanner.toml") +TRIVY_SCA_SARIF_OUTPUT = os.getenv("TRIVY_SCA_SARIF_OUTPUT", "sca-trivy-container.sarif") +OSV_SCA_SARIF_OUTPUT = os.getenv("OSV_SCA_SARIF_OUTPUT", "sca-osv-container.sarif") + +# --- SAST/Code Linting (OpenGrep + Hadolint) --- +SEMGREP_RULES_DIR = os.getenv("SEMGREP_RULES_DIR", "/opt/semgrep-rules") +OPENGREP_SAST_SARIF_OUTPUT = os.getenv("OPENGREP_SAST_SARIF_OUTPUT", "sast-opengrep-dockerfile.sarif") +HADOLINT_SAST_SARIF_OUTPUT = os.getenv("HADOLINT_SAST_SARIF_OUTPUT", "sast-hadolint-dockerfile.sarif") + +# --- Functions to run each SCA tool and handle their outputs +def run_trivy(): + cmd = [ + "trivy", "image", + IMAGE_NAME, + "--format", "sarif", + "--ignorefile", TRIVY_IGNOREFILE, + "--output", TRIVY_SCA_SARIF_OUTPUT + ] + return subprocess.run(cmd).returncode + +def run_osv_scanner(): + cmd = [ + "osv-scanner", "scan", "image", + IMAGE_NAME, + "--config", OSV_IGNOREFILE, + "--format", "sarif", + "--output-file", OSV_SCA_SARIF_OUTPUT + ] + exit_code = subprocess.run(cmd).returncode + if exit_code == 1: + return 0 # OSV Scanner returns 1 if vulnerabilities are found, but we want to continue the pipeline + return exit_code + +def handle_sca(): + + tools = {"trivy": run_trivy, "osv-scanner": run_osv_scanner} + sarif_files = {"trivy": TRIVY_SCA_SARIF_OUTPUT, "osv-scanner": OSV_SCA_SARIF_OUTPUT} + tool_status = {} # "PASSED" | "WARNING" | "FAILED" | "ERROR" + gate_failed = False + + # Run each SCA tool and collect their exit codes + for name, tool_fn in tools.items(): + exit_code = tool_fn() + logger.info("-" * 40) + + path = sarif_files[name] + if exit_code != 0 and os.path.exists(path): + logger.error(f"{RED}[!] {name} exit code {exit_code} but wrote {path}{RESET}") + tool_status[name] = "ERROR" + gate_failed = True + + # Evaluate each SARIF file for gate decision + for name, path in sarif_files.items(): + if name in tool_status: + continue # already flagged ERROR above, don't overwrite it + + if not os.path.exists(path): + logger.error(f"{RED}[!] {name} SARIF missing: {path},tool failed to run (not a vulnerability){RESET}") + tool_status[name] = "ERROR" + gate_failed = True + continue + + eval_result = evaluate(path) + + if eval_result.gate_failed: + tool_status[name] = "FAILED" # this tool found CVSS >= 8.0 + gate_failed = True + elif eval_result.gate_warn: + tool_status[name] = "WARNING" # this tool found 5.0 <= CVSS < 8.0 + else: + tool_status[name] = "PASSED" # this tool found nothing >= 5.0 + + # Print summary of results + logger.info(f"\n{BOLD}========== SCA PIPELINE SUMMARY =========={RESET}") + for name, status in tool_status.items(): + if status == "PASSED": + logger.info(f"[{name}]: {GREEN}PASSED{RESET}") + elif status == "WARNING": + logger.warning(f"[{name}]: {YELLOW}WARNING (findings between 5.0 and 8.0){RESET}") + elif status == "ERROR": + logger.error(f"[{name}]: {RED}ERROR (tool did not run correctly){RESET}") + else: + logger.error(f"[{name}]: {RED}FAILED (CVSS >= 8.0 found){RESET}") + logger.info(f"{BOLD}=========================================={RESET}\n") + + # Exit with non-zero code if any tool failed the gate + if gate_failed: + logger.error(f"{RED}One or more SCA tools failed the gate check.{RESET}") + sys.exit(1) + + +# --- Functions to run each SAST tool and handle their outputs +def run_hadolint(): + cmd = [ + "hadolint", "Dockerfile", + "--failure-threshold", "error", + "--format", "sarif", + ] + with open(HADOLINT_SAST_SARIF_OUTPUT, "w") as f: + result = subprocess.run(cmd, stdout=f) + + return result.returncode + + +def run_semgrep(): + cmd = [ + "opengrep", "scan", + "--config", "p/dockerfile", + "--config", SEMGREP_RULES_DIR, + "--include", "Dockerfile", + "--severity=ERROR", + "--error", + "--sarif", + "--output", OPENGREP_SAST_SARIF_OUTPUT, + ] + return subprocess.run(cmd).returncode + +def handle_sast(): + tools = { + "hadolint": run_hadolint, + "semgrep": run_semgrep, + } + + tool_status = {} + + for name, run_fn in tools.items(): + exit_code = run_fn() + if exit_code == 0: + tool_status[name] = "PASSED" + elif exit_code == 1: + tool_status[name] = "FAILED" + else: + tool_status[name] = "ERROR" + + logger.info(f"\n{BOLD}========== SAST PIPELINE SUMMARY =========={RESET}") + for name, status in tool_status.items(): + if status == "PASSED": + logger.info(f"[{name}]: {GREEN}PASSED (exit code 0){RESET}") + elif status == "FAILED": + logger.error(f"[{name}]: {RED}FAILED (exit code 1 - error-severity findings){RESET}") + else: + logger.error(f"[{name}]: {RED}ERROR (exit code {exit_code}, tool did not run correctly){RESET}") + logger.info(f"{BOLD}==========================================={RESET}\n") + + if any(status != "PASSED" for status in tool_status.values()): + sys.exit(1) + +def merge_sarifs(sarif_paths, output_path): + merged = { + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [], + } + + for path in sarif_paths: + if not os.path.exists(path): + logger.warning(f"{path} not found, skipping in merge") + continue + with open(path) as f: + sarif = json.load(f, strict=False) + merged["runs"].extend(sarif.get("runs", [])) + + with open(output_path, "w") as f: + json.dump(merged, f) + + logger.info(f"SARIF files merged successfully into {output_path}") + +def main(): + parser = argparse.ArgumentParser( + prog="sec-orchestrator", + description="Agnostic DevSecOps Container scannning Pipeline Orchestrator" + ) + + # The primary router + parser.add_argument( + "-s", "--scan-type", + choices=["sast", "sca"], + help="Specify the security methodology to execute (e.g., sast, sca)" + ) + + # Image flag specifically SCA approach + parser.add_argument( + "-i", "--image", + help="Target Docker image reference" + ) + + parser.add_argument( + "--merge-sarif", + nargs="+", + metavar="SARIF_FILE", + help="List of SARIF files to merge into one report" + ) + + parser.add_argument( + "--merge-output", + default="merged-container-scan.sarif", + help="Output path for the merged SARIF file" + ) + args = parser.parse_args() + + if args.scan_type == "sast": + logger.info(f"{BOLD}Initiating SAST pipeline: {RESET}") + handle_sast() + # Execution + elif args.scan_type == "sca": + logger.info(f"{BOLD}Initiating SCA pipeline on {args.image}{RESET}") + global IMAGE_NAME + if args.image: # Override the global IMAGE_NAME + IMAGE_NAME = args.image + handle_sca() + + if args.merge_sarif: + merge_sarifs(args.merge_sarif, args.merge_output) + return + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/scripts/run_sca_image.py b/.github/scripts/run_sca_image.py deleted file mode 100644 index 76cee1bf..00000000 --- a/.github/scripts/run_sca_image.py +++ /dev/null @@ -1,132 +0,0 @@ -import subprocess -import os -import sys -import logging -import json -from parse_sarif import evaluate - -GREEN = '\033[92m' -RED = '\033[91m' -RESET = '\033[0m' -BOLD = '\033[1m' -YELLOW = '\033[93m' - -logging.basicConfig( - level=logging.INFO, - format='%(message)s' # Clean format to prevent double-timestamps in CI logs -) -logger = logging.getLogger("sca-orchestrator") - -# --- Configurable values -IMAGE_NAME = os.getenv("IMAGE_NAME", "platform-backend:local") -TRIVY_IGNOREFILE = os.getenv("TRIVY_IGNOREFILE", ".github/scripts/suppress_trivy.yaml") -OSV_IGNOREFILE = os.getenv("OSV_IGNOREFILE", ".github/scripts/suppress_osv_scanner.toml") -TRIVY_SARIF_OUTPUT = os.getenv("TRIVY_SARIF_OUTPUT", "trivy-image.sarif") -OSV_SARIF_OUTPUT = os.getenv("OSV_SARIF_OUTPUT", "osv-scanner-image.sarif") -MERGED_SARIF_OUTPUT = os.getenv("MERGED_SARIF_OUTPUT", "merged-SCA-platform-backend-image.sarif") - -def run_trivy(): - cmd = [ - "trivy", "image", - IMAGE_NAME, - "--format", "sarif", - "--ignorefile", TRIVY_IGNOREFILE, - "--output", TRIVY_SARIF_OUTPUT - ] - return subprocess.run(cmd).returncode - -def run_osv_scanner(): - cmd = [ - "osv-scanner", "scan", "image", - IMAGE_NAME, - "--config", OSV_IGNOREFILE, - "--format", "sarif", - "--output-file", OSV_SARIF_OUTPUT - ] - exit_code = subprocess.run(cmd).returncode - if exit_code == 1: - return 0 # OSV Scanner returns 1 if vulnerabilities are found, but we want to continue the pipeline - return exit_code - -def merge_sarifs(): - merged = { - "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", - "version": "2.1.0", - "runs": [], - } - - for path in (TRIVY_SARIF_OUTPUT, OSV_SARIF_OUTPUT): - if not os.path.exists(path): - logger.warning(f"{path} not found, skipping in merge") - continue - with open(path) as f: - sarif = json.load(f, strict=False) - merged["runs"].extend(sarif.get("runs", [])) - - with open(MERGED_SARIF_OUTPUT, "w") as f: - json.dump(merged, f) - - logger.info("SARIF files merged successfully.") - -def main(): - - tools = {"trivy": run_trivy, "osv-scanner": run_osv_scanner} - sarif_files = {"trivy": TRIVY_SARIF_OUTPUT, "osv-scanner": OSV_SARIF_OUTPUT} - tool_status = {} # "PASSED" | "WARNING" | "FAILED" | "ERROR" - gate_failed = False - - # Run each SCA tool and collect their exit codes - for name, tool_fn in tools.items(): - exit_code = tool_fn() - logger.info("-" * 40) - - path = sarif_files[name] - if exit_code != 0 and os.path.exists(path): - logger.error(f"{RED}[!] {name} exit code {exit_code} but wrote {path}{RESET}") - tool_status[name] = "ERROR" - gate_failed = True - - - merge_sarifs() # combined artifact only, not used for the gate decision - - # Evaluate each SARIF file for gate decision - for name, path in sarif_files.items(): - if name in tool_status: - continue # already flagged ERROR above, don't overwrite it - - if not os.path.exists(path): - logger.error(f"{RED}[!] {name} SARIF missing: {path},tool failed to run (not a vulnerability){RESET}") - tool_status[name] = "ERROR" - gate_failed = True - continue - - eval_result = evaluate(path) - - if eval_result.gate_failed: - tool_status[name] = "FAILED" # this tool found CVSS >= 8.0 - gate_failed = True - elif eval_result.gate_warn: - tool_status[name] = "WARNING" # this tool found 5.0 <= CVSS < 8.0 - else: - tool_status[name] = "PASSED" # this tool found nothing >= 5.0 - - # Print summary of results - logger.info(f"\n{BOLD}========== SCA PIPELINE SUMMARY =========={RESET}") - for name, status in tool_status.items(): - if status == "PASSED": - logger.info(f"[{name}]: {GREEN}PASSED{RESET}") - elif status == "WARNING": - logger.warning(f"[{name}]: {YELLOW}WARNING (findings between 5.0 and 8.0){RESET}") - elif status == "ERROR": - logger.error(f"[{name}]: {RED}ERROR (tool did not run correctly){RESET}") - else: - logger.error(f"[{name}]: {RED}FAILED (CVSS >= 8.0 found){RESET}") - logger.info(f"{BOLD}=========================================={RESET}\n") - - # Exit with non-zero code if any tool failed the gate - if gate_failed: - logger.error(f"{RED}One or more SCA tools failed the gate check.{RESET}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/.github/scripts/sast_scan.py b/.github/scripts/sast_scan.py new file mode 100644 index 00000000..fa6f64f2 --- /dev/null +++ b/.github/scripts/sast_scan.py @@ -0,0 +1,77 @@ +import subprocess +import os +import sys +import logging + +GREEN = '\033[92m' +RED = '\033[91m' +RESET = '\033[0m' +BOLD = '\033[1m' +YELLOW = '\033[93m' + +logging.basicConfig( + level=logging.INFO, + format='%(message)s' # Clean format to prevent double-timestamps in CI logs +) +logger = logging.getLogger("sast-orchestrator") + +# --- Configurable values ----------------------------------------------- +SEMGREP_CONFIG_RULESETS = os.getenv( + "SEMGREP_CONFIG_RULESETS", + "opt/semgrep-rules" +).split() +OPENGREP_EXCLUDE = os.getenv( + "OPENGREP_EXCLUDE", + ".github/scripts Dockerfile" +).split() +OPENGREP_SARIF_OUTPUT = os.getenv("OPENGREP_SARIF_OUTPUT", "sast-opengrep-app.sarif") + +def run_opengrep(): + base_cmd = ["opengrep", "scan"] + \ + [f"--config {config}" for config in SEMGREP_CONFIG_RULESETS] + \ + [f"--exclude={pattern}" for pattern in OPENGREP_EXCLUDE] + + report_cmd = (base_cmd + ["--sarif", "--output", OPENGREP_SARIF_OUTPUT]) + report_cmd = " ".join(report_cmd).split() + logger.info(f"{BOLD}Running (report):{RESET} {' '.join(report_cmd)}") + subprocess.run(report_cmd) + + gate_cmd = (base_cmd + ["--severity=ERROR", "--error"]) + gate_cmd = " ".join(gate_cmd).split() + logger.info(f"{BOLD}Running (gate):{RESET} {' '.join(gate_cmd)}") + return subprocess.run(gate_cmd).returncode + +def main(): + + exit_code = run_opengrep() + logger.info("-" * 40) + + if exit_code == 0: + status = "PASSED" + elif exit_code == 1: + status = "FAILED" + else: + status = "ERROR" + + if not os.path.exists(OPENGREP_SARIF_OUTPUT): + logger.error(f"{RED}[!] opengrep SARIF missing: {OPENGREP_SARIF_OUTPUT}, tool failed to run{RESET}") + status = "ERROR" + + logger.info(f"\n{BOLD}========== SAST PIPELINE SUMMARY =========={RESET}") + if status == "PASSED": + logger.info(f"[opengrep]: {GREEN}PASSED (exit code 0){RESET}") + elif status == "FAILED": + logger.error(f"[opengrep]: {RED}FAILED (exit code 1 - {SAST_SEVERITY}-severity findings){RESET}") + else: + logger.error(f"[opengrep]: {RED}ERROR (exit code {exit_code}, tool did not run correctly){RESET}") + logger.info(f"{BOLD}==========================================={RESET}\n") + + if status == "ERROR": + sys.exit(1) + + if status == "FAILED": + logger.error(f"{RED}SAST gate failed: blocking-severity findings present.{RESET}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/scripts/run_sca_app.py b/.github/scripts/sca_scan.py similarity index 91% rename from .github/scripts/run_sca_app.py rename to .github/scripts/sca_scan.py index f15f16b7..62441785 100644 --- a/.github/scripts/run_sca_app.py +++ b/.github/scripts/sca_scan.py @@ -21,14 +21,13 @@ SBOM_PATH = os.getenv("SBOM_PATH", "target/bom.json") TRIVY_IGNOREFILE = os.getenv("TRIVY_IGNOREFILE", ".github/scripts/suppress_trivy.yaml") OSV_IGNOREFILE = os.getenv("OSV_IGNOREFILE", ".github/scripts/suppress_osv_scanner.toml") -TRIVY_SARIF_OUTPUT = os.getenv("TRIVY_SARIF_OUTPUT", "trivy-app.sarif") -OSV_SARIF_OUTPUT = os.getenv("OSV_SARIF_OUTPUT", "osv-scanner-app.sarif") -MERGED_SARIF_OUTPUT = os.getenv("MERGED_SARIF_OUTPUT", "merged-SCA-platform-backend-app.sarif") +TRIVY_SARIF_OUTPUT = os.getenv("TRIVY_SARIF_OUTPUT", "trivy-platform-backend.sarif") +OSV_SARIF_OUTPUT = os.getenv("OSV_SARIF_OUTPUT", "osv-scanner-platform-backend.sarif") +SCA_MERGED_SARIF_OUTPUT = os.getenv("SCA_MERGED_SARIF_OUTPUT", "SCA-platform-backend-merged.sarif") def run_trivy(): cmd = [ - "trivy", "sbom", - SBOM_PATH, + "trivy", "sbom", SBOM_PATH, "--format", "sarif", "--ignorefile", TRIVY_IGNOREFILE, "--output", TRIVY_SARIF_OUTPUT @@ -37,7 +36,7 @@ def run_trivy(): def run_osv_scanner(): cmd = [ - "osv-scanner", "scan", "source", + "osv-scanner", "scan", "source", "--lockfile", SBOM_PATH, "--config", OSV_IGNOREFILE, "--format", "sarif", @@ -63,7 +62,7 @@ def merge_sarifs(): sarif = json.load(f, strict=False) merged["runs"].extend(sarif.get("runs", [])) - with open(MERGED_SARIF_OUTPUT, "w") as f: + with open(SCA_MERGED_SARIF_OUTPUT, "w") as f: json.dump(merged, f) logger.info("SARIF files merged successfully.") diff --git a/.github/scripts/setup-tools.sh b/.github/scripts/setup-tools.sh index ae6a9fc5..35be1f0a 100644 --- a/.github/scripts/setup-tools.sh +++ b/.github/scripts/setup-tools.sh @@ -5,7 +5,6 @@ set -u # treat unset variables as an error trap 'echo "[setup-tools] ERROR: command failed (exit $?) at line $LINENO: $BASH_COMMAND" >&2' ERR - # Tool versions and the SHA256 of the release asset we download. # The "# renovate:" markers let Renovate bump version and checksum together # (see renovate.json). When overriding a *_VERSION via env, the matching @@ -19,12 +18,49 @@ TRIVY_SHA256="${TRIVY_SHA256:-3cbae37cd440cd8676e5ce9207fe460b5641c7579a17e9d00f OSV_SCANNER_VERSION="${OSV_SCANNER_VERSION:-v2.4.0}" OSV_SCANNER_SHA256="${OSV_SCANNER_SHA256:-15314940c10d26af9c6649f150b8a47c1262e8fc7e17b1d1029b0e479e8ed8a0}" +# renovate: datasource=github-release-attachments depName=opengrep/opengrep +OPENGREP_VERSION="${OPENGREP_VERSION:-v1.25.0}" +OPENGREP_SHA256="${OPENGREP_SHA256:-9ac4aebb47ba3f7b0d8fc641ac8749cb6c2f253f616131a67d9631e00d4bea33}" + +# renovate: datasource=github-tags depName=semgrep/semgrep-rules +SEMGREP_RULES_REF="${SEMGREP_RULES_REF:-bf362e1642cc2a16ca44bcae0fdda78639e383c3}" +SEMGREP_RULES_DIR="/opt/semgrep-rules" + +# renovate: datasource=github-release-attachments depName=hadolint/hadolint +HADOLINT_VERSION="${HADOLINT_VERSION:-v2.14.0}" +HADOLINT_SHA256="${HADOLINT_SHA256:-6bf226944684f56c84dd014e8b979d27425c0148f61b3bd99bcc6f39e9dc5a47}" + # renovate: datasource=npm depName=@cyclonedx/cyclonedx-npm CYCLONEDX_NPM_VERSION="${CYCLONEDX_NPM_VERSION:-6.0.0}" TMP_DIR="$(mktemp -d)" trap 'rm -rf "${TMP_DIR}"' EXIT +# --- Flag parsing ----------------------------------------------------- +INSTALL_TOOL="all" +SBOM_ECOSYSTEM="none" + +while [[ $# -gt 0 ]]; do + case "$1" in + --install-tool) + INSTALL_TOOL="$2" + shift 2 + ;; + --sbom-ecosystem) + SBOM_ECOSYSTEM="$2" + shift 2 + ;; + *) + echo "[setup-tools] Unknown flag: $1" >&2 + exit 1 + ;; + esac +done + +should_install() { + [[ "$INSTALL_TOOL" == "all" || ",$INSTALL_TOOL," == *",$1,"* ]] +} + # Download a file and refuse to proceed unless its SHA256 matches the pinned one download_and_verify() { local url="$1" dest="$2" sha256="$3" @@ -32,32 +68,66 @@ download_and_verify() { echo "${sha256} ${dest}" | sha256sum -c - } -# Installing Trivy from the release tarball (no install.sh piped from an unpinned branch) -echo "[setup-tools] Installing Trivy ${TRIVY_VERSION}" -TRIVY_TARBALL="trivy_${TRIVY_VERSION#v}_Linux-64bit.tar.gz" -download_and_verify \ - "https://github.com/aquasecurity/trivy/releases/download/${TRIVY_VERSION}/${TRIVY_TARBALL}" \ - "${TMP_DIR}/${TRIVY_TARBALL}" \ - "${TRIVY_SHA256}" -sudo tar -xzf "${TMP_DIR}/${TRIVY_TARBALL}" -C /usr/local/bin trivy -trivy --version -echo "Trivy installed OK" - -# Installing OSV Scanner -echo "[setup-tools] Installing OSV Scanner ${OSV_SCANNER_VERSION}" -download_and_verify \ - "https://github.com/google/osv-scanner/releases/download/${OSV_SCANNER_VERSION}/osv-scanner_linux_amd64" \ - "${TMP_DIR}/osv-scanner" \ - "${OSV_SCANNER_SHA256}" -sudo install -m 0755 "${TMP_DIR}/osv-scanner" /usr/local/bin/osv-scanner -osv-scanner --version -echo "OSV Scanner installed OK" - - -# Generate SBOM based on project type -PROJECT_TYPE="${1:-none}" # maven | npm | none - -case "$PROJECT_TYPE" in +# --- Trivy -------------------------------------------------------------- +if should_install "trivy"; then + echo "[setup-tools] Installing Trivy ${TRIVY_VERSION}" + TRIVY_TARBALL="trivy_${TRIVY_VERSION#v}_Linux-64bit.tar.gz" + download_and_verify \ + "https://github.com/aquasecurity/trivy/releases/download/${TRIVY_VERSION}/${TRIVY_TARBALL}" \ + "${TMP_DIR}/${TRIVY_TARBALL}" \ + "${TRIVY_SHA256}" + sudo tar -xzf "${TMP_DIR}/${TRIVY_TARBALL}" -C /usr/local/bin trivy + trivy --version + echo "Trivy installed OK" +fi + +# --- OSV Scanner ---------------------------------------------------------- +if should_install "osv-scanner"; then + echo "[setup-tools] Installing OSV Scanner ${OSV_SCANNER_VERSION}" + download_and_verify \ + "https://github.com/google/osv-scanner/releases/download/${OSV_SCANNER_VERSION}/osv-scanner_linux_amd64" \ + "${TMP_DIR}/osv-scanner" \ + "${OSV_SCANNER_SHA256}" + sudo install -m 0755 "${TMP_DIR}/osv-scanner" /usr/local/bin/osv-scanner + osv-scanner --version + echo "OSV Scanner installed OK" +fi + +# --- OpenGrep ------------------------------------------------------------- +if should_install "opengrep"; then + echo "[setup-tools] Installing OpenGrep ${OPENGREP_VERSION}" + download_and_verify \ + "https://github.com/opengrep/opengrep/releases/download/${OPENGREP_VERSION}/opengrep_manylinux_x86" \ + "${TMP_DIR}/opengrep" \ + "${OPENGREP_SHA256}" + sudo install -m 0755 "${TMP_DIR}/opengrep" /usr/local/bin/opengrep + opengrep --version + echo "OpenGrep installed OK" +fi + +# --- Semgrep community ruleset (cloned, not registry) --------- +if should_install "semgrep-rules"; then + echo "[setup-tools] Cloning semgrep-rules @ ${SEMGREP_RULES_REF}" + sudo rm -rf "${SEMGREP_RULES_DIR}" + sudo git clone --quiet https://github.com/semgrep/semgrep-rules.git "${SEMGREP_RULES_DIR}" + sudo git -C "${SEMGREP_RULES_DIR}" checkout --quiet "${SEMGREP_RULES_REF}" + echo "semgrep-rules ready at ${SEMGREP_RULES_DIR} (ref: ${SEMGREP_RULES_REF})" +fi + +# --- Hadolint --------------------------------------------------------- +if should_install "hadolint"; then + echo "[setup-tools] Installing Hadolint ${HADOLINT_VERSION}" + download_and_verify \ + "https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-linux-x86_64" \ + "${TMP_DIR}/hadolint" \ + "${HADOLINT_SHA256}" + sudo install -m 0755 "${TMP_DIR}/hadolint" /usr/local/bin/hadolint + hadolint --version + echo "Hadolint installed OK" +fi + +# --- SBOM generation ---------------------------------------------------- +case "$SBOM_ECOSYSTEM" in maven) echo "Generating SBOM for Maven project" mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -q @@ -70,7 +140,7 @@ case "$PROJECT_TYPE" in echo "No SBOM generation needed" ;; *) - echo "Unknown PROJECT_TYPE: $PROJECT_TYPE" >&2 # redirect error message to stderr + echo "Unknown SBOM_ECOSYSTEM: $SBOM_ECOSYSTEM" >&2 exit 1 ;; -esac +esac \ No newline at end of file diff --git a/.github/workflows/container-scan.yml b/.github/workflows/container-scan.yml new file mode 100644 index 00000000..4e9ef8bf --- /dev/null +++ b/.github/workflows/container-scan.yml @@ -0,0 +1,101 @@ +name: Container Vulnerability Scanning + +on: + push: # To be removed after testing + pull_request: + workflow_dispatch: + schedule: + - cron: '0 2 * * 1' # weekly SCA scan, every Monday 02:00 UTC + +jobs: + container-scan: + name: Container Vulnerability Scanning + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # required for uploading SCA results to github security + env: + IMAGE_NAME: platform-backend:testing + CONTAINER_SCAN_MERGED_SARIF_OUTPUT: container-scan-platform-backend-merged.sarif + + # SCA / CVE + TRIVY_IGNOREFILE: .github/scripts/suppress_trivy.yaml + OSV_IGNOREFILE: .github/scripts/suppress_osv_scanner.toml + TRIVY_SCA_SARIF_OUTPUT: sca-trivy-container.sarif + OSV_SCA_SARIF_OUTPUT: sca-osv-container.sarif + SCA_MERGED_SARIF_OUTPUT: sca-merged-container.sarif + + # SAST /Linting + OPENGREP_SAST_SARIF_OUTPUT: sast-opengrep-dockerfile.sarif + HADOLINT_SAST_SARIF_OUTPUT: sast-hadolint-dockerfile.sarif + + steps: + - name: Check out repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 + with: + python-version: '3.14.4' + + - name: Build Docker image + run: docker build -t ${{ env.IMAGE_NAME }} . + + - name: Setup tools + run: | + bash .github/scripts/setup-tools.sh \ + --install-tool trivy,osv-scanner,opengrep,hadolint,semgrep-rules + + - name: Run SAST scanning + run: python .github/scripts/container_scan.py --scan-type sast + + - name: Run SCA scanning + if: always() + run: python .github/scripts/container_scan.py --scan-type sca --image ${{ env.IMAGE_NAME }} + + - name: Upload Trivy SARIF to GitHub Security tab + id: upload_trivy + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: ${{ env.TRIVY_SCA_SARIF_OUTPUT }} + category: trivy-container-scanning + + - name: Upload OSV Scanner SARIF to GitHub Security tab + id: upload_osv + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: ${{ env.OSV_SCA_SARIF_OUTPUT }} + category: osv-scanner-container-scanning + + - name: Upload OpenGrep SARIF to GitHub Security tab + id: upload_opengrep + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: ${{ env.OPENGREP_SAST_SARIF_OUTPUT }} + category: opengrep-sast + + - name: Upload Hadolint SARIF to GitHub Security tab + id: upload_hadolint + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: ${{ env.HADOLINT_SAST_SARIF_OUTPUT }} + category: hadolint-sast + + - name: Merge all SARIF reports + if: always() + run: | + python .github/scripts/container_scan.py \ + --merge-sarif "${{ env.TRIVY_SCA_SARIF_OUTPUT }}" "${{ env.OSV_SCA_SARIF_OUTPUT }}" "${{ env.OPENGREP_SAST_SARIF_OUTPUT }}" "${{ env.HADOLINT_SAST_SARIF_OUTPUT }}" \ + --merge-output "${{ env.CONTAINER_SCAN_MERGED_SARIF_OUTPUT }}" + + - name: Upload SARIF artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: container-scan-sarif-report + path: ${{ env.CONTAINER_SCAN_MERGED_SARIF_OUTPUT }} + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml new file mode 100644 index 00000000..2e20c8a3 --- /dev/null +++ b/.github/workflows/sast.yml @@ -0,0 +1,50 @@ +name: Static Application Security Testing (SAST) + +on: + pull_request: + workflow_dispatch: + schedule: + - cron: '0 2 * * 1' # weekly SCA scan, every Monday 02:00 UTC + +jobs: + sast: + name: Static Application Security Testing + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # required for uploading SAST results to github security + env: + SEMGREP_CONFIG_RULESETS: "opt/semgrep-rules/generic opt/semgrep-rules/problem-based-packs opt/semgrep-rules/bash opt/semgrep-rules/java" + OPENGREP_EXCLUDE: ".github/scripts Dockerfile*" + OPENGREP_SARIF_OUTPUT: sast-semgrep-app.sarif + + steps: + - name: Check out repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 + with: + python-version: '3.14.4' + + - name: Setup tools + run: bash .github/scripts/setup-tools.sh --install-tool opengrep,semgrep-rules + + - name: Run SAST scanning + run: python .github/scripts/sast_scan.py + + - name: Upload Semgrep SARIF to GitHub Security tab + id: upload_semgrep + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: ${{ env.OPENGREP_SARIF_OUTPUT }} + category: semgrep-app + + - name: Upload SARIF artifact + if: ${{ always() && steps.upload_semgrep.outcome == 'success' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 + with: + name: sast-scan-sarif-report + path: ${{ env.OPENGREP_SARIF_OUTPUT }} + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/sca_app.yml b/.github/workflows/sca.yml similarity index 76% rename from .github/workflows/sca_app.yml rename to .github/workflows/sca.yml index 098154a9..51d8f854 100644 --- a/.github/workflows/sca_app.yml +++ b/.github/workflows/sca.yml @@ -1,4 +1,4 @@ -name: SCA CI - app +name: Software Composition Analysis (SCA) on: pull_request: @@ -17,9 +17,9 @@ jobs: SBOM_PATH: target/bom.json TRIVY_IGNOREFILE: .github/scripts/suppress_trivy.yaml OSV_IGNOREFILE: .github/scripts/suppress_osv_scanner.toml - TRIVY_SARIF_OUTPUT: trivy-app.sarif - OSV_SARIF_OUTPUT: osv-scanner-app.sarif - MERGED_SARIF_OUTPUT: merged-SCA-platform-backend-app.sarif + TRIVY_SARIF_OUTPUT: trivy-platform-backend.sarif + OSV_SARIF_OUTPUT: osv-scanner-platform-backend.sarif + SCA_MERGED_SARIF_OUTPUT: SCA-platform-backend-merged.sarif steps: - name: Check out repository @@ -41,10 +41,10 @@ jobs: run: mvn dependency:resolve -q - name: Setup tools - run: bash .github/scripts/setup-tools.sh maven + run: bash .github/scripts/setup-tools.sh --install-tool trivy,osv-scanner --sbom-ecosystem maven - name: Run SCA tools - run: python .github/scripts/run_sca_app.py + run: python .github/scripts/sca_scan.py - name: Upload Trivy SARIF to GitHub Security tab id: upload_trivy @@ -62,9 +62,10 @@ jobs: sarif_file: ${{ env.OSV_SARIF_OUTPUT }} category: osv-scanner-app - - name: Upload merged SARIF to GitHub Security tab + - name: Upload SARIF artifacts if: ${{ always() && steps.upload_trivy.outcome == 'success' && steps.upload_osv.outcome == 'success' }} - uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: - sarif_file: ${{ env.MERGED_SARIF_OUTPUT }} - category: merged-sca-app + name: sca-scan-sarif-report + path: ${{ env.SCA_MERGED_SARIF_OUTPUT }} + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/sca_image.yml b/.github/workflows/sca_image.yml deleted file mode 100644 index 5a9dd2db..00000000 --- a/.github/workflows/sca_image.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: SCA CI - image - -on: - pull_request: - workflow_dispatch: - schedule: - - cron: '0 2 * * 1' # weekly SCA scan, every Monday 02:00 UTC - -jobs: - sca: - name: Software Composition Analysis - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write # required for uploading SCA results to github security - env: - IMAGE_NAME: platform-backend:testing - TRIVY_IGNOREFILE: .github/scripts/suppress_trivy.yaml - OSV_IGNOREFILE: .github/scripts/suppress_osv_scanner.toml - TRIVY_SARIF_OUTPUT: trivy-image.sarif - OSV_SARIF_OUTPUT: osv-scanner-image.sarif - MERGED_SARIF_OUTPUT: merged-SCA-platform-backend-image.sarif - - steps: - - name: Check out repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 - with: - python-version: '3.14.4' - - - name: Build Docker image - run: docker build -t ${{ env.IMAGE_NAME }} . - - - name: Setup tools - run: bash .github/scripts/setup-tools.sh - - - name: Run SCA tools - run: python .github/scripts/run_sca_image.py - - - name: Upload Trivy SARIF to GitHub Security tab - id: upload_trivy - if: always() - uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 - with: - sarif_file: ${{ env.TRIVY_SARIF_OUTPUT }} - category: trivy-image - - - name: Upload OSV Scanner SARIF to GitHub Security tab - id: upload_osv - if: always() - uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 - with: - sarif_file: ${{ env.OSV_SARIF_OUTPUT }} - category: osv-scanner-image - - - name: Upload merged SARIF to GitHub Security tab - if: ${{ always() && steps.upload_trivy.outcome == 'success' && steps.upload_osv.outcome == 'success' }} - uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 - with: - sarif_file: ${{ env.MERGED_SARIF_OUTPUT }} - category: merged-sca-image diff --git a/Dockerfile b/Dockerfile index 2fb4aa23..490731c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ RUN apk add --no-cache curl ####################################################### # Install dockerize ####################################################### -ENV DOCKERIZE_VERSION=v0.13.0 +ENV DOCKERIZE_VERSION=v0.14.0 RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz diff --git a/pom.xml b/pom.xml index 39587c28..69cfbf0a 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ UTF-8 21 - 42.7.11 + 42.7.12 3.2.0 7.4.0.Final 12.7.0