From 125d8edba8f8caa2e0f560fa5e9a678e19629be0 Mon Sep 17 00:00:00 2001 From: Val Redchenko Date: Fri, 30 Jan 2026 21:04:16 +0000 Subject: [PATCH 1/2] feat: add local vulnerability scanning scripts - osv-scan-repos.sh: multi-language scanner using osv-scanner Go binary - pip-audit-scan-repos.sh: Python-only scanner using uvx pip-audit Both scripts scan ERIC workspace repos by default and support markdown output via -o flag. Relates to #153 --- scripts/osv-scan-repos.sh | 164 ++++++++++++++++++++++++++++++++ scripts/pip-audit-scan-repos.sh | 158 ++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100755 scripts/osv-scan-repos.sh create mode 100755 scripts/pip-audit-scan-repos.sh diff --git a/scripts/osv-scan-repos.sh b/scripts/osv-scan-repos.sh new file mode 100755 index 0000000..0e115ef --- /dev/null +++ b/scripts/osv-scan-repos.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# +# Local OSV vulnerability scanner for ERIC workspace repos +# Scans specified repositories and outputs results to stdout or a file +# +# Usage: +# ./osv-scan-repos.sh # Scan default repos, output to stdout +# ./osv-scan-repos.sh -o results.md # Output as markdown to file +# ./osv-scan-repos.sh repo1 repo2 # Scan specific repo paths +# +# Requirements: +# - osv-scanner binary in PATH or at ~/.asdf/installs/golang/*/bin/osv-scanner + +set -euo pipefail + +# Find osv-scanner binary +find_osv_scanner() { + if command -v osv-scanner &>/dev/null; then + echo "osv-scanner" + return + fi + + # Check asdf golang installations + local asdf_bin + asdf_bin=$(find ~/.asdf/installs/golang -name "osv-scanner" -type f 2>/dev/null | head -1) + if [[ -n "$asdf_bin" ]]; then + echo "$asdf_bin" + return + fi + + echo "ERROR: osv-scanner not found. Install with: go install github.com/google/osv-scanner/cmd/osv-scanner@latest" >&2 + exit 1 +} + +OSV_SCANNER=$(find_osv_scanner) + +# Default repos relative to ERIC workspace root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ERIC_ROOT="${SCRIPT_DIR}/../../../.." + +DEFAULT_REPOS=( + "repos/DiamondLightSource/smartem-decisions" + "repos/DiamondLightSource/smartem-frontend" + "repos/DiamondLightSource/smartem-devtools" + "repos/DiamondLightSource/fandanGO-cryoem-dls" + "repos/FragmentScreen/fandanGO-aria" + "repos/FragmentScreen/fandanGO-core" + "repos/FragmentScreen/fandanGO-cryoem-cnb" + "repos/FragmentScreen/fandanGO-nmr-cerm" + "repos/FragmentScreen/fandanGO-nmr-guf" + "repos/FragmentScreen/ddapi-record-logs" + "repos/FragmentScreen/ddapi-record-oscem" +) + +OUTPUT_FILE="" +REPOS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -o|--output) + OUTPUT_FILE="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [-o output.md] [repo_path ...]" + echo "" + echo "Options:" + echo " -o, --output FILE Write results to FILE (markdown format)" + echo " -h, --help Show this help" + echo "" + echo "If no repos specified, scans all ERIC workspace repos." + exit 0 + ;; + *) + REPOS+=("$1") + shift + ;; + esac +done + +# Use default repos if none specified +if [[ ${#REPOS[@]} -eq 0 ]]; then + for repo in "${DEFAULT_REPOS[@]}"; do + REPOS+=("${ERIC_ROOT}/${repo}") + done +fi + +scan_repo() { + local repo_path="$1" + local repo_name + repo_name=$(basename "$repo_path") + + if [[ ! -d "$repo_path" ]]; then + echo "SKIP: $repo_name (directory not found)" + return + fi + + echo "Scanning: $repo_name" + "$OSV_SCANNER" --recursive "$repo_path" 2>&1 || true +} + +scan_repo_markdown() { + local repo_path="$1" + local repo_name + repo_name=$(basename "$repo_path") + + if [[ ! -d "$repo_path" ]]; then + echo "### $repo_name" + echo "" + echo "**Status:** Skipped (directory not found)" + echo "" + return + fi + + echo "### $repo_name" + echo "" + + local output + output=$("$OSV_SCANNER" --recursive "$repo_path" 2>&1) || true + + if echo "$output" | grep -q "No issues found"; then + echo "**Status:** Clean - no vulnerabilities found" + elif echo "$output" | grep -q "OSV URL"; then + echo "**Status:** Vulnerabilities found" + echo "" + echo '```' + echo "$output" | grep -A1000 "OSV URL" | head -50 + echo '```' + else + echo "**Status:** No dependencies detected or scan error" + echo "" + echo '```' + echo "$output" | tail -10 + echo '```' + fi + echo "" +} + +# Run scans +if [[ -n "$OUTPUT_FILE" ]]; then + { + echo "# OSV Scanner Results" + echo "" + echo "**Scan date:** $(date -Iseconds)" + echo "" + echo "**Scanner version:** $("$OSV_SCANNER" --version 2>&1 | head -1)" + echo "" + echo "---" + echo "" + + for repo in "${REPOS[@]}"; do + scan_repo_markdown "$repo" + done + } > "$OUTPUT_FILE" + + echo "Results written to: $OUTPUT_FILE" +else + for repo in "${REPOS[@]}"; do + echo "========================================" + scan_repo "$repo" + echo "" + done +fi diff --git a/scripts/pip-audit-scan-repos.sh b/scripts/pip-audit-scan-repos.sh new file mode 100755 index 0000000..b6e4a69 --- /dev/null +++ b/scripts/pip-audit-scan-repos.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# +# Local pip-audit vulnerability scanner for ERIC workspace repos (Python only) +# Scans specified repositories and outputs results to stdout or a file +# Uses uvx to run pip-audit without permanent installation +# +# Usage: +# ./pip-audit-scan-repos.sh # Scan default repos, output to stdout +# ./pip-audit-scan-repos.sh -o results.md # Output as markdown to file +# ./pip-audit-scan-repos.sh repo1 repo2 # Scan specific repo paths +# +# Requirements: +# - uvx (from uv package manager) + +set -euo pipefail + +# Default repos relative to ERIC workspace root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ERIC_ROOT="${SCRIPT_DIR}/../../../.." + +DEFAULT_REPOS=( + "repos/DiamondLightSource/smartem-decisions" + "repos/DiamondLightSource/smartem-frontend" + "repos/DiamondLightSource/smartem-devtools" + "repos/DiamondLightSource/fandanGO-cryoem-dls" + "repos/FragmentScreen/fandanGO-aria" + "repos/FragmentScreen/fandanGO-core" + "repos/FragmentScreen/fandanGO-cryoem-cnb" + "repos/FragmentScreen/fandanGO-nmr-cerm" + "repos/FragmentScreen/fandanGO-nmr-guf" + "repos/FragmentScreen/ddapi-record-logs" + "repos/FragmentScreen/ddapi-record-oscem" +) + +OUTPUT_FILE="" +REPOS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -o|--output) + OUTPUT_FILE="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [-o output.md] [repo_path ...]" + echo "" + echo "Options:" + echo " -o, --output FILE Write results to FILE (markdown format)" + echo " -h, --help Show this help" + echo "" + echo "If no repos specified, scans all ERIC workspace Python repos." + echo "Note: Only scans Python repos with requirements.txt files." + exit 0 + ;; + *) + REPOS+=("$1") + shift + ;; + esac +done + +# Use default repos if none specified +if [[ ${#REPOS[@]} -eq 0 ]]; then + for repo in "${DEFAULT_REPOS[@]}"; do + REPOS+=("${ERIC_ROOT}/${repo}") + done +fi + +scan_repo() { + local repo_path="$1" + local repo_name + repo_name=$(basename "$repo_path") + + if [[ ! -d "$repo_path" ]]; then + echo "SKIP: $repo_name (directory not found)" + return + fi + + if [[ ! -f "$repo_path/requirements.txt" ]]; then + echo "SKIP: $repo_name (no requirements.txt)" + return + fi + + echo "Scanning: $repo_name" + uvx pip-audit -r "$repo_path/requirements.txt" 2>&1 || true +} + +scan_repo_markdown() { + local repo_path="$1" + local repo_name + repo_name=$(basename "$repo_path") + + echo "### $repo_name" + echo "" + + if [[ ! -d "$repo_path" ]]; then + echo "**Status:** Skipped (directory not found)" + echo "" + return + fi + + if [[ ! -f "$repo_path/requirements.txt" ]]; then + echo "**Status:** Skipped (no requirements.txt - not a Python project or uses different dep format)" + echo "" + return + fi + + local output + output=$(uvx pip-audit -r "$repo_path/requirements.txt" 2>&1) || true + + if echo "$output" | grep -q "No known vulnerabilities found"; then + echo "**Status:** Clean - no vulnerabilities found" + elif echo "$output" | grep -q "found [0-9]* known vulnerabilit"; then + local vuln_count + vuln_count=$(echo "$output" | grep -oP "found \K[0-9]+" | head -1) + echo "**Status:** $vuln_count vulnerabilities found" + echo "" + echo '```' + echo "$output" + echo '```' + else + echo "**Status:** Scan completed" + echo "" + echo '```' + echo "$output" + echo '```' + fi + echo "" +} + +# Run scans +if [[ -n "$OUTPUT_FILE" ]]; then + { + echo "# pip-audit Scanner Results" + echo "" + echo "**Scan date:** $(date -Iseconds)" + echo "" + echo "**Scanner:** pip-audit (via uvx)" + echo "" + echo "**Note:** pip-audit only scans Python dependencies (requirements.txt). PHP repos are skipped." + echo "" + echo "---" + echo "" + + for repo in "${REPOS[@]}"; do + scan_repo_markdown "$repo" + done + } > "$OUTPUT_FILE" + + echo "Results written to: $OUTPUT_FILE" +else + for repo in "${REPOS[@]}"; do + echo "========================================" + scan_repo "$repo" + echo "" + done +fi From 5f9a8d698b9c00d6dead856b2ca0b7dd1cbaf8d6 Mon Sep 17 00:00:00 2001 From: Val Redchenko Date: Thu, 5 Feb 2026 16:36:23 +0000 Subject: [PATCH 2/2] replace default list of repos to scan with just all repos in workspace for both scanning scripts --- scripts/osv-scan-repos.sh | 28 +++++++++++----------------- scripts/pip-audit-scan-repos.sh | 28 +++++++++++----------------- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/scripts/osv-scan-repos.sh b/scripts/osv-scan-repos.sh index 0e115ef..f9e5f7e 100755 --- a/scripts/osv-scan-repos.sh +++ b/scripts/osv-scan-repos.sh @@ -38,19 +38,13 @@ OSV_SCANNER=$(find_osv_scanner) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ERIC_ROOT="${SCRIPT_DIR}/../../../.." -DEFAULT_REPOS=( - "repos/DiamondLightSource/smartem-decisions" - "repos/DiamondLightSource/smartem-frontend" - "repos/DiamondLightSource/smartem-devtools" - "repos/DiamondLightSource/fandanGO-cryoem-dls" - "repos/FragmentScreen/fandanGO-aria" - "repos/FragmentScreen/fandanGO-core" - "repos/FragmentScreen/fandanGO-cryoem-cnb" - "repos/FragmentScreen/fandanGO-nmr-cerm" - "repos/FragmentScreen/fandanGO-nmr-guf" - "repos/FragmentScreen/ddapi-record-logs" - "repos/FragmentScreen/ddapi-record-oscem" -) +# Discover all repos in ERIC workspace (repos/*/* directories with .git) +discover_repos() { + local repos_dir="${ERIC_ROOT}/repos" + if [[ -d "$repos_dir" ]]; then + find "$repos_dir" -mindepth 2 -maxdepth 2 -type d -exec test -d '{}/.git' \; -print | sort + fi +} OUTPUT_FILE="" REPOS=() @@ -79,11 +73,11 @@ while [[ $# -gt 0 ]]; do esac done -# Use default repos if none specified +# Use discovered repos if none specified if [[ ${#REPOS[@]} -eq 0 ]]; then - for repo in "${DEFAULT_REPOS[@]}"; do - REPOS+=("${ERIC_ROOT}/${repo}") - done + while IFS= read -r repo; do + REPOS+=("$repo") + done < <(discover_repos) fi scan_repo() { diff --git a/scripts/pip-audit-scan-repos.sh b/scripts/pip-audit-scan-repos.sh index b6e4a69..0bea9b9 100755 --- a/scripts/pip-audit-scan-repos.sh +++ b/scripts/pip-audit-scan-repos.sh @@ -18,19 +18,13 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ERIC_ROOT="${SCRIPT_DIR}/../../../.." -DEFAULT_REPOS=( - "repos/DiamondLightSource/smartem-decisions" - "repos/DiamondLightSource/smartem-frontend" - "repos/DiamondLightSource/smartem-devtools" - "repos/DiamondLightSource/fandanGO-cryoem-dls" - "repos/FragmentScreen/fandanGO-aria" - "repos/FragmentScreen/fandanGO-core" - "repos/FragmentScreen/fandanGO-cryoem-cnb" - "repos/FragmentScreen/fandanGO-nmr-cerm" - "repos/FragmentScreen/fandanGO-nmr-guf" - "repos/FragmentScreen/ddapi-record-logs" - "repos/FragmentScreen/ddapi-record-oscem" -) +# Discover all repos in ERIC workspace (repos/*/* directories with .git) +discover_repos() { + local repos_dir="${ERIC_ROOT}/repos" + if [[ -d "$repos_dir" ]]; then + find "$repos_dir" -mindepth 2 -maxdepth 2 -type d -exec test -d '{}/.git' \; -print | sort + fi +} OUTPUT_FILE="" REPOS=() @@ -60,11 +54,11 @@ while [[ $# -gt 0 ]]; do esac done -# Use default repos if none specified +# Use discovered repos if none specified if [[ ${#REPOS[@]} -eq 0 ]]; then - for repo in "${DEFAULT_REPOS[@]}"; do - REPOS+=("${ERIC_ROOT}/${repo}") - done + while IFS= read -r repo; do + REPOS+=("$repo") + done < <(discover_repos) fi scan_repo() {