From a316a7e90e29a5cbb52fab1873b9ed67b857f53a Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:39:31 +1000 Subject: [PATCH 1/2] Update core.py, main.py, and OWASP workflow --- .github/workflows/owasp.yml | 67 +++++++++++++++++++++++ scanner/core.py | 104 +++++++++++++++++------------------- scanner/main.py | 40 +++++++++----- 3 files changed, 142 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/owasp.yml diff --git a/.github/workflows/owasp.yml b/.github/workflows/owasp.yml new file mode 100644 index 0000000..745e6fe --- /dev/null +++ b/.github/workflows/owasp.yml @@ -0,0 +1,67 @@ +name: OWASP PR Scanner + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + scan: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR HEAD + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install deps + run: | + python -m pip install -U pip + if [ -f scanner/requirements.txt ]; then + pip install -r scanner/requirements.txt + fi + + - name: Run OWASP Scanner + id: owasp + run: | + python scanner/main.py > scan_output.txt + if grep -q "Severity" scan_output.txt; then + echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT + else + echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT + fi + + - name: Post PR Comment + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body-path: scan_output.txt + + - name: Upload scan artifact + uses: actions/upload-artifact@v4 + with: + name: owasp-scan-results + path: scan_output.txt + retention-days: 5 + + - name: Fail if vulnerabilities found + if: steps.owasp.outputs.vulnerabilities_found == 'true' + run: | + echo "::error::❌ Vulnerabilities detected! Merge blocked." + exit 1 + + - name: Safe to merge + if: steps.owasp.outputs.vulnerabilities_found == 'false' + run: | + echo "✅ No vulnerabilities found. Safe to merge." diff --git a/scanner/core.py b/scanner/core.py index f48deb8..6da4ecd 100644 --- a/scanner/core.py +++ b/scanner/core.py @@ -1,10 +1,3 @@ -# Responsibilities: -# - Reads target file, stores code lines -# - Manages vulnerability list -# - Runs all rule checks (auto-discovers rules in scanner/rules) -# - Provides add_vulnerability callback -# - Prints a grouped, colourised report - import os import importlib import pkgutil @@ -21,7 +14,6 @@ def _load_rule_modules(): if hasattr(mod, "check"): modules.append(mod) - # Stable order: by CATEGORY "A01: ..." if provided, else by module name def key(m): cat = getattr(m, "CATEGORY", "") head = cat.split(":", 1)[0].strip() if cat else "" @@ -61,7 +53,6 @@ def parse_file(self): def run_checks(self): for rule in RULE_MODULES: - # each rule exposes: check(code_lines, add_vulnerability) rule.check(self.code_lines, self.add_vulnerability) def run(self): @@ -70,48 +61,47 @@ def run(self): self.run_checks() def report(self): - # ---- colour helpers ---- + """Outputs results with colors locally, or clean Markdown when in GitHub Actions.""" def supports_truecolor() -> bool: return os.environ.get("COLORTERM", "").lower() in ("truecolor", "24bit") + disable_color = os.environ.get("GITHUB_ACTIONS") == "true" + def rgb(r, g, b) -> str: return f"\033[38;2;{r};{g};{b}m" ANSI = { - "reset": "\033[0m", - "bold": "\033[1m", - "cyan": "\033[96m", - "magenta": "\033[95m", - "yellow": "\033[93m", - "red": "\033[91m", - "green": "\033[92m", - "blue": "\033[94m", + "reset": "" if disable_color else "\033[0m", + "bold": "" if disable_color else "\033[1m", + "cyan": "" if disable_color else "\033[96m", + "magenta": "" if disable_color else "\033[95m", + "yellow": "" if disable_color else "\033[93m", + "red": "" if disable_color else "\033[91m", + "green": "" if disable_color else "\033[92m", + "blue": "" if disable_color else "\033[94m", } - TRUECOLOR = supports_truecolor() - - # Severity colours (true-color -> fallback) - CRIT = (rgb(220, 20, 60) if TRUECOLOR else ANSI["red"] + ANSI["bold"]) # crimson - HIGH = (rgb(255, 0, 0) if TRUECOLOR else ANSI["red"]) # red - MED = (rgb(255, 165, 0) if TRUECOLOR else ANSI["yellow"]) # orange-ish - LOW = (rgb(0, 200, 0) if TRUECOLOR else ANSI["green"]) # green - - RESET = ANSI["reset"] - BOLD = ANSI["bold"] - HDR = (rgb(180, 130, 255) if TRUECOLOR else ANSI["magenta"]) # section header - TITLE = (rgb(120, 220, 200) if TRUECOLOR else ANSI["cyan"]) # title - SUM = (rgb(255, 215, 0) if TRUECOLOR else ANSI["yellow"]) # summary label + TRUECOLOR = supports_truecolor() and not disable_color - sev_color = {"CRITICAL": CRIT, "HIGH": HIGH, "MEDIUM": MED, "LOW": LOW} + sev_color = { + "CRITICAL": "**CRITICAL**" if disable_color else (rgb(220, 20, 60) if TRUECOLOR else ANSI["red"] + ANSI["bold"]), + "HIGH": "**HIGH**" if disable_color else (rgb(255, 0, 0) if TRUECOLOR else ANSI["red"]), + "MEDIUM": "**MEDIUM**" if disable_color else (rgb(255, 165, 0) if TRUECOLOR else ANSI["yellow"]), + "LOW": "**LOW**" if disable_color else (rgb(0, 200, 0) if TRUECOLOR else ANSI["green"]), + } - print(f"\n{BOLD}{TITLE}Scan Results for {self.file_path}:{RESET}") + # ---- Print header ---- + if disable_color: + print(f"\n### 🔒 OWASP Scanner Results for `{self.file_path}`") + else: + print(f"\n{ANSI['bold']}{ANSI['cyan']}Scan Results for {self.file_path}:{ANSI['reset']}") if not self.vulnerabilities: - ok = rgb(0, 200, 0) if TRUECOLOR else ANSI["green"] - print(f"{ok}✅ No vulnerabilities found.{RESET}") + msg = "✅ No vulnerabilities found." + print(msg) return - # Group by category + # ---- Group by category ---- groups = {} for v in self.vulnerabilities: groups.setdefault(v["category"], []).append(v) @@ -122,25 +112,29 @@ def cat_key(cat: str): for cat in sorted(groups.keys(), key=cat_key): items = sorted(groups[cat], key=lambda x: x["line"]) - # tally sev_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0} for v in items: - sev_counts[v["severity"]] = sev_counts.get(v["severity"], 0) + 1 - - total = len(items) - print(f"\n{BOLD}{HDR}=== {cat} ({total} finding{'s' if total != 1 else ''}) ==={RESET}") - - chips = [] - for k in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]: - n = sev_counts.get(k, 0) - if n: - chips.append(f"{sev_color[k]}{k.title()}{RESET}: {n}") - if chips: - print(f"{SUM}Summary:{RESET} " + ", ".join(chips)) - + sev_counts[v["severity"]] += 1 + + if disable_color: + print(f"\n#### {cat} ({len(items)} findings)") + chips = [] + for k in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]: + if sev_counts[k]: + chips.append(f"{k}: {sev_counts[k]}") + if chips: + print(f"**Summary:** " + ", ".join(chips)) + else: + print(f"\n{ANSI['bold']}{ANSI['magenta']}=== {cat} ({len(items)} findings) ==={ANSI['reset']}") + + # ---- List individual vulnerabilities ---- for v in items: - sc = sev_color.get(v["severity"], ANSI["blue"]) - print(f"\n {BOLD}• Line {v['line']} |{RESET} " - f"Severity {sc}{v['severity']}{RESET} | " - f"Confidence {v['confidence']}") - print(f" → {v['description']}") + sev = sev_color.get(v["severity"], v["severity"]) + if disable_color: + print(f"- Line {v['line']} | Severity {sev} | Confidence {v['confidence']}") + print(f" → {v['description']}") + else: + print(f" {ANSI['bold']}• Line {v['line']} |{ANSI['reset']} " + f"Severity {sev}{ANSI['reset']} | " + f"Confidence {v['confidence']}") + print(f" → {v['description']}") diff --git a/scanner/main.py b/scanner/main.py index 7bc4d88..714ea75 100644 --- a/scanner/main.py +++ b/scanner/main.py @@ -1,21 +1,33 @@ -# Entry point for the OWASP PR Scanner CLI tool. -# This script parses the command-line arguments (i.e., the file path to scan), -# initializes the VulnerabilityScanner with the specified file, runs all rule checks, -# and prints a formatted vulnerability report to the console. +import sys +import os +from scanner.core import VulnerabilityScanner -import argparse -from .core import VulnerabilityScanner +def main(file_paths): + any_vulns = False + for file_path in file_paths: + scanner = VulnerabilityScanner(file_path) + if not scanner.parse_file(): + if os.environ.get("GITHUB_ACTIONS") == "true": + print(f"\n### ⚠️ File `{file_path}` not found") + else: + print(f"\n[!] File {file_path} does not exist.") + continue -def main(): - parser = argparse.ArgumentParser(description="OWASP PR Vulnerability Scanner") - parser.add_argument("path", help="Path to Python file to scan") - args = parser.parse_args() + scanner.run_checks() + scanner.report() + + if scanner.vulnerabilities: + any_vulns = True + + if any_vulns: + sys.exit(1) - scanner = VulnerabilityScanner(args.path) - scanner.run() - scanner.report() if __name__ == "__main__": - main() + if len(sys.argv) < 2: + print("Usage: python scanner/main.py ...") + sys.exit(1) + + main(sys.argv[1:]) From 8333ef5c0a0d414faee36815c073a870f8614681 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:47:50 +1000 Subject: [PATCH 2/2] Update owasp.yml Re-added changed files if statement --- .github/workflows/owasp.yml | 78 ++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/.github/workflows/owasp.yml b/.github/workflows/owasp.yml index 745e6fe..8aed2c3 100644 --- a/.github/workflows/owasp.yml +++ b/.github/workflows/owasp.yml @@ -30,29 +30,95 @@ jobs: python -m pip install -U pip if [ -f scanner/requirements.txt ]; then pip install -r scanner/requirements.txt + elif [ -f requirements.txt ]; then + pip install -r requirements.txt fi - - name: Run OWASP Scanner + - name: Determine changed files for this PR + id: diff + run: | + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + RAW="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true)" + APP_CHANGED="$(echo "$RAW" \ + | grep -E '\.(js|jsx|ts|tsx|py|java|go|rb|php|html|css|md|conf|yml|yaml|json)$' \ + || true)" + if [ -z "$APP_CHANGED" ]; then + APP_CHANGED="$(git ls-files)" + fi + echo "changed_files<> $GITHUB_OUTPUT + echo "$APP_CHANGED" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Run OWASP scanner id: owasp run: | - python scanner/main.py > scan_output.txt - if grep -q "Severity" scan_output.txt; then + CHANGED_FILES="${{ steps.diff.outputs.changed_files }}" + if [ -z "$CHANGED_FILES" ]; then + echo "Nothing to scan." | tee owasp-results.txt + echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT + exit 0 + fi + + if [ ! -d "scanner" ]; then + echo "::error::Scanner module not found (scanner/)." + exit 1 + fi + + : > owasp-results.txt + EXIT=0 + while IFS= read -r file; do + [ -z "$file" ] && continue + echo "### File: $file" >> owasp-results.txt + echo '```' >> owasp-results.txt + python -m scanner.main "$file" >> owasp-results.txt 2>&1 || EXIT=1 + echo '```' >> owasp-results.txt + echo "" >> owasp-results.txt + done <<< "$CHANGED_FILES" + + if [ $EXIT -ne 0 ]; then echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT else echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT fi - - name: Post PR Comment + - name: Create PR comment body + if: always() + run: | + RESULTS=$(cat owasp-results.txt || echo "No results.") + if [ "${{ steps.owasp.outputs.vulnerabilities_found }}" == "true" ]; then + echo 'comment_body<> $GITHUB_ENV + echo '## 🔒 OWASP Scanner Results' >> $GITHUB_ENV + echo '' >> $GITHUB_ENV + echo 'Vulnerabilities were detected:' >> $GITHUB_ENV + echo '```' >> $GITHUB_ENV + echo "$RESULTS" >> $GITHUB_ENV + echo '```' >> $GITHUB_ENV + echo '⛔ Please address these before merging.' >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + else + echo 'comment_body<> $GITHUB_ENV + echo '## 🔒 OWASP Scanner Results' >> $GITHUB_ENV + echo '' >> $GITHUB_ENV + echo 'No vulnerabilities detected.' >> $GITHUB_ENV + echo '```' >> $GITHUB_ENV + echo "$RESULTS" >> $GITHUB_ENV + echo '```' >> $GITHUB_ENV + echo '✅ Good to go.' >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + fi + + - name: Comment PR uses: peter-evans/create-or-update-comment@v4 with: issue-number: ${{ github.event.pull_request.number }} - body-path: scan_output.txt + body: ${{ env.comment_body }} - name: Upload scan artifact uses: actions/upload-artifact@v4 with: name: owasp-scan-results - path: scan_output.txt + path: owasp-results.txt retention-days: 5 - name: Fail if vulnerabilities found