From e25a7aacbc956688f6e6affc8fd38b22448a9005 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:39:27 +1000 Subject: [PATCH 01/15] Initial commit --- .gitignore | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 17 +---- 2 files changed, 208 insertions(+), 16 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7faf40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,207 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/README.md b/README.md index 70a0b50..2519bac 100644 --- a/README.md +++ b/README.md @@ -1,16 +1 @@ -# Cyber-Security - -This repository is the main repository for the Cyber Security Team. Whilst general files should go here, full projects within the Cyber Security team should be split-off into their own repository within the Redback Operations company (under the Cyber team, ensure Tutors have **admin** and cyber team **write** permissions) to avoid bloat in this central repository. - - -- Research folder contains generic research not relevant to a particular trimester. - -- Otherwise, each trimester folder contains small projects / trials conducted. - -- Documentation links for associated docs are scattered were relevant documentation exists. - -- Some 2022 files yet to be moved over. - -- If you are creating documentation or a research piece, please create a .md equivalent and add to the [documentation repo](https://github.com/Redback-Operations/redback-documentation) - -- [General doc site here](https://redback-operations.github.io/redback-documentation/docs/category/cyber-security-team). \ No newline at end of file +# owasp-scanner \ No newline at end of file From b8eb2cc4bb655ce28c4c258f8559f06b9c79c3c0 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:25:07 +1000 Subject: [PATCH 02/15] SQL injection test --- requirements.txt | 3 ++ scanner/__init__.py | 0 scanner/core.py | 53 ++++++++++++++++++++++++++++++++++ scanner/main.py | 20 +++++++++++++ scanner/rules/__init__.py | 0 scanner/rules/sql_injection.py | 27 +++++++++++++++++ tests/test_negative.py | 11 +++++++ tests/test_positive.py | 11 +++++++ 8 files changed, 125 insertions(+) create mode 100644 requirements.txt create mode 100644 scanner/__init__.py create mode 100644 scanner/core.py create mode 100644 scanner/main.py create mode 100644 scanner/rules/__init__.py create mode 100644 scanner/rules/sql_injection.py create mode 100644 tests/test_negative.py create mode 100644 tests/test_positive.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6c924a7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# This file lists all dependencies needed to run the scanner. +# To install the requirements: +# pip install -r requirements.txt \ No newline at end of file diff --git a/scanner/__init__.py b/scanner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scanner/core.py b/scanner/core.py new file mode 100644 index 0000000..f4422eb --- /dev/null +++ b/scanner/core.py @@ -0,0 +1,53 @@ +# Responsibilities: +# - Reads the target Python file and stores code lines. +# - Manages the vulnerability list. +# - Coordinates execution of all defined rule checks (e.g., SQL injection, XSS). +# - Provides an interface (`add_vulnerability`) for rules to report findings. +# - Generates a user-friendly vulnerability report after scanning. + +# To extend functionality, add new rule modules to scanner/rules and call them in + +import os +from scanner.rules import sql_injection # Import your first rule here + +class VulnerabilityScanner: + def __init__(self, file_path): + self.file_path = file_path + self.code_lines = [] + self.vulnerabilities = [] + + def add_vulnerability(self, category, description, line, severity, confidence): + self.vulnerabilities.append({ + "category": category, + "description": description, + "line": line, + "severity": severity, + "confidence": confidence + }) + + def parse_file(self): + if not os.path.exists(self.file_path): + print(f"File {self.file_path} does not exist.") + return False + with open(self.file_path, "r", encoding="utf-8") as f: + self.code_lines = f.readlines() + return True + + def run_checks(self): + # Add each rule here + sql_injection.check(self.code_lines, self.add_vulnerability) + + def run(self): + if not self.parse_file(): + return + self.run_checks() + + def report(self): + print(f"\nScan Results for {self.file_path}:") + if not self.vulnerabilities: + print("✅ No vulnerabilities found.") + else: + for vuln in self.vulnerabilities: + print(f"\n⚠️ {vuln['category']} at line {vuln['line']}") + print(f" → {vuln['description']}") + print(f" Severity: {vuln['severity']} | Confidence: {vuln['confidence']}") diff --git a/scanner/main.py b/scanner/main.py new file mode 100644 index 0000000..e25de56 --- /dev/null +++ b/scanner/main.py @@ -0,0 +1,20 @@ +# 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 argparse +from scanner.core import VulnerabilityScanner + +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 = VulnerabilityScanner(args.path) + scanner.run() + scanner.report() + +if __name__ == "__main__": + main() diff --git a/scanner/rules/__init__.py b/scanner/rules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scanner/rules/sql_injection.py b/scanner/rules/sql_injection.py new file mode 100644 index 0000000..999660e --- /dev/null +++ b/scanner/rules/sql_injection.py @@ -0,0 +1,27 @@ +# This module implements a check for OWASP A01: Injection vulnerabilities. + +# Specifically, it searches for suspicious SQL query patterns in Python code, +# such as unparameterized queries or string concatenation in `execute()` calls. + +# Function: +# - `check(code_lines, add_vulnerability)`: Accepts lines of code and a callback to report findings. +# Uses regular expressions to detect potential SQLi and sends alerts via `add_vulnerability()`. + +import re + +def check(code_lines, add_vulnerability): + sqli_patterns = [ + r'(?i)cursor\.execute\([^,]+["\'].*?(SELECT|INSERT|DELETE|UPDATE).*?["\']', + r'(?i)execute\([^,]+["\'].*?(SELECT|INSERT|DELETE|UPDATE).*?["\']', + r'(?i)"\s*\+\s*[\w\[]+.*\+\s*"' + ] + for i, line in enumerate(code_lines): + for pattern in sqli_patterns: + if re.search(pattern, line): + add_vulnerability( + "A01: Injection", + f"Potential SQL injection: {line.strip()}", + i + 1, + "HIGH", + "HIGH" + ) diff --git a/tests/test_negative.py b/tests/test_negative.py new file mode 100644 index 0000000..b767788 --- /dev/null +++ b/tests/test_negative.py @@ -0,0 +1,11 @@ +# This file should produce clean results + + +import sqlite3 + +username = input("Enter your username: ") +query = "SELECT * FROM users WHERE username = ?" + +conn = sqlite3.connect("example.db") +cursor = conn.cursor() +cursor.execute(query, (username,)) \ No newline at end of file diff --git a/tests/test_positive.py b/tests/test_positive.py new file mode 100644 index 0000000..b6afb8a --- /dev/null +++ b/tests/test_positive.py @@ -0,0 +1,11 @@ +# This file should trigger the SQL injection rule + +import sqlite3 + +username = input("Enter your username: ") +query = "SELECT * FROM users WHERE username = '" + username + "'" + +conn = sqlite3.connect("example.db") +cursor = conn.cursor() + +cursor.execute(query) \ No newline at end of file From 1b15a0eab82d3827d2c64ada20fff2f2d4bd68a0 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:31:25 +1000 Subject: [PATCH 03/15] adding a readme file --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2519bac..67f10b0 100644 --- a/README.md +++ b/README.md @@ -1 +1,54 @@ -# owasp-scanner \ No newline at end of file +# OWASP PR Scanner + +This tool is designed to scan Python files for security vulnerabilities based on the OWASP Top 10. + +--- + +## ✅ Current Functionality + +The scanner detects potential vulnerabilities using static analysis (regex and basic logic). Currently implemented: + +- **A01: Injection** + - Detects unparameterized SQL queries + - Flags SQL built with string concatenation or f-strings + +- **Clean vs Vulnerable Detection** + - Example `test_positive.py` will trigger an alert for SQL injection + - Example `test_negative.py` is safe and produces no alerts + +--- + +## 🚧 Planned Features (OWASP Top 10 Coverage) +This scanner will be extended to cover the full OWASP Top 10. + +Currently implemented: + +✅ A01: Injection (e.g. SQL injection detection using regex) + +Planned: + +A02: Cryptographic Failures (e.g. weak hashing, insecure SSL use) + +A03: Injection – more types (e.g. XSS, Command Injection) + +A04: Insecure Design (e.g. missing access controls) + +A05: Security Misconfiguration (e.g. debug mode, missing headers) + +A06: Vulnerable and Outdated Components (dependency scanning) + +A07: Identification and Authentication Failures (e.g. missing auth checks) + +A08: Software and Data Integrity Failures (e.g. unsafe deserialization) + +A09: Security Logging and Monitoring Failures (e.g. no logging in sensitive flows) + +A10: Server-Side Request Forgery (SSRF) + + +## 👤 Author +Developed by Liana Perry (2025) +Cybersecurity SecDevOps Sub-team | Redback Operations + +## 🙌 Acknowledgements +This project is inspired by the original vulnerability scanning logic created by Amir Zandieh, and extends it into a modular and OWASP-aligned security scanning tool for pull requests. \ No newline at end of file From b322c29d96de87fe7d7b1f7c8834845b36f6dd17 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 6 Aug 2025 18:51:56 +1000 Subject: [PATCH 04/15] update read me --- README.md | 9 +++++++++ scanner/main.py | 3 ++- scanner/rules/sql_injection.py | 30 +++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 67f10b0..712c441 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,15 @@ A09: Security Logging and Monitoring Failures (e.g. no logging in sensitive flow A10: Server-Side Request Forgery (SSRF) +## Running the Script +### 1. Navigate to your project root +cd path/to/owasp-scanner + +### 2. Set PYTHONPATH so Python recognizes `scanner/` as a package +set PYTHONPATH=. + +### 3. Run the script with the file to scan as an argument +python scanner/main.py tests/test_positive.py ## 👤 Author Developed by Liana Perry (2025) diff --git a/scanner/main.py b/scanner/main.py index e25de56..7201f2a 100644 --- a/scanner/main.py +++ b/scanner/main.py @@ -5,7 +5,8 @@ import argparse -from scanner.core import VulnerabilityScanner +from core import VulnerabilityScanner + def main(): parser = argparse.ArgumentParser(description="OWASP PR Vulnerability Scanner") diff --git a/scanner/rules/sql_injection.py b/scanner/rules/sql_injection.py index 999660e..519e4bb 100644 --- a/scanner/rules/sql_injection.py +++ b/scanner/rules/sql_injection.py @@ -10,18 +10,30 @@ import re def check(code_lines, add_vulnerability): - sqli_patterns = [ - r'(?i)cursor\.execute\([^,]+["\'].*?(SELECT|INSERT|DELETE|UPDATE).*?["\']', - r'(?i)execute\([^,]+["\'].*?(SELECT|INSERT|DELETE|UPDATE).*?["\']', - r'(?i)"\s*\+\s*[\w\[]+.*\+\s*"' - ] + assigned_queries = {} + for i, line in enumerate(code_lines): - for pattern in sqli_patterns: - if re.search(pattern, line): + if re.search(r"=\s*['\"]\s*(SELECT|INSERT|UPDATE|DELETE)", line, re.IGNORECASE) and '+' in line: + var_match = re.match(r"\s*(\w+)\s*=", line) + if var_match: + var_name = var_match.group(1) + assigned_queries[var_name] = i + 1 + add_vulnerability( "A01: Injection", - f"Potential SQL injection: {line.strip()}", + f"SQL query created via string concatenation: {line.strip()}", i + 1, "HIGH", - "HIGH" + "MEDIUM" ) + + # Detect execution of those suspicious queries + for var_name in assigned_queries: + if f"execute({var_name})" in line: + add_vulnerability( + "A01: Injection", + f"Suspicious query passed to execute(): {line.strip()}", + i + 1, + "HIGH", + "HIGH" + ) \ No newline at end of file From 05d0b828c55f37351be01e0e4ca54719a3d45e40 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:03:30 +1000 Subject: [PATCH 05/15] adding 2 more rules and making the results more readable --- scanner/core.py | 116 +++++++++++++++++---- scanner/rules/broken_access_control.py | 94 +++++++++++++++++ scanner/rules/security_misconfiguration.py | 86 +++++++++++++++ tests/test_negative.py | 21 +++- tests/test_positive.py | 34 +++++- 5 files changed, 328 insertions(+), 23 deletions(-) create mode 100644 scanner/rules/broken_access_control.py create mode 100644 scanner/rules/security_misconfiguration.py diff --git a/scanner/core.py b/scanner/core.py index f4422eb..8742ea4 100644 --- a/scanner/core.py +++ b/scanner/core.py @@ -1,14 +1,18 @@ # Responsibilities: -# - Reads the target Python file and stores code lines. -# - Manages the vulnerability list. -# - Coordinates execution of all defined rule checks (e.g., SQL injection, XSS). -# - Provides an interface (`add_vulnerability`) for rules to report findings. -# - Generates a user-friendly vulnerability report after scanning. - -# To extend functionality, add new rule modules to scanner/rules and call them in +# - Reads target file, stores code lines +# - Manages vulnerability list +# - Runs all rule checks +# - Provides add_vulnerability callback +# - Prints a simple report import os -from scanner.rules import sql_injection # Import your first rule here +from scanner.rules import sql_injection, broken_access_control, security_misconfiguration + +RULE_MODULES = [ + sql_injection, + broken_access_control, + security_misconfiguration, +] class VulnerabilityScanner: def __init__(self, file_path): @@ -22,7 +26,7 @@ def add_vulnerability(self, category, description, line, severity, confidence): "description": description, "line": line, "severity": severity, - "confidence": confidence + "confidence": confidence, }) def parse_file(self): @@ -34,8 +38,9 @@ def parse_file(self): return True def run_checks(self): - # Add each rule here - sql_injection.check(self.code_lines, self.add_vulnerability) + for rule in RULE_MODULES: + # each rule exposes: check(code_lines, add_vulnerability) + rule.check(self.code_lines, self.add_vulnerability) def run(self): if not self.parse_file(): @@ -43,11 +48,86 @@ def run(self): self.run_checks() def report(self): - print(f"\nScan Results for {self.file_path}:") + import os + + # ---- colour helpers ---- + def supports_truecolor() -> bool: + # Most modern terminals set COLORTERM=truecolor or 24bit + return os.environ.get("COLORTERM", "").lower() in ("truecolor", "24bit") + + def rgb(r, g, b) -> str: + return f"\033[38;2;{r};{g};{b}m" + + # Fallback 8/16-colour palette + 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", + } + + 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 + + sev_color = { + "CRITICAL": CRIT, + "HIGH": HIGH, + "MEDIUM": MED, + "LOW": LOW, + } + + print(f"\n{BOLD}{TITLE}Scan Results for {self.file_path}:{RESET}") + if not self.vulnerabilities: - print("✅ No vulnerabilities found.") - else: - for vuln in self.vulnerabilities: - print(f"\n⚠️ {vuln['category']} at line {vuln['line']}") - print(f" → {vuln['description']}") - print(f" Severity: {vuln['severity']} | Confidence: {vuln['confidence']}") + ok = rgb(0, 200, 0) if TRUECOLOR else ANSI["green"] + print(f"{ok}✅ No vulnerabilities found.{RESET}") + return + + # Group by category + groups = {} + for v in self.vulnerabilities: + groups.setdefault(v["category"], []).append(v) + + def cat_key(cat: str): + # Sort A01..A10 first, then alphabetically + head = cat.split(":", 1)[0].strip() + return (0, int(head[1:])) if head.startswith("A") and head[1:].isdigit() else (1, cat.lower()) + + 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}") + + # coloured summary chips + 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)) + + # entries + 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']}") diff --git a/scanner/rules/broken_access_control.py b/scanner/rules/broken_access_control.py new file mode 100644 index 0000000..2897c04 --- /dev/null +++ b/scanner/rules/broken_access_control.py @@ -0,0 +1,94 @@ +# This module implements a check for OWASP A02: Broken Access Control. +# +# It looks for common patterns that suggest missing or weak authorization checks: +# 1) Flask routes without an auth/role decorator (e.g., @login_required, @jwt_required). +# 2) Django REST Framework endpoints that explicitly allow unauthenticated access +# (e.g., permission_classes = [AllowAny]). +# 3) Express.js routes that attach a handler directly with no middleware +# (e.g., app.get('/admin', (req, res) => ...)) which often implies no auth check. +# +# Function: +# - `check(code_lines, add_vulnerability)`: Scans lines and reports findings with context. + +import re + +AUTH_DECORATOR_RE = re.compile( + r'@(login_required|jwt_required|roles_required|requires_auth|auth_required|permission_required)', + re.IGNORECASE, +) +FLASK_ROUTE_RE = re.compile(r'@(?:\w+\.)?route\s*\(', re.IGNORECASE) +DEF_RE = re.compile(r'^\s*def\s+\w+\s*\(', re.IGNORECASE) + +DRF_ALLOWANY_RE = re.compile(r'permission_classes\s*=\s*\[\s*AllowAny\s*\]') +DRF_IMPORT_ALLOWANY_RE = re.compile(r'from\s+rest_framework\.permissions\s+import\s+.*AllowAny', re.IGNORECASE) + +# app.get('/path', handler) or router.post("/path", handler) +# If there is a direct callback right after the path, there is probably no middleware. +EXPRESS_ROUTE_RE = re.compile( + r'\b(?:app|router)\.(get|post|put|patch|delete|options|head)\s*\(\s*[\'"][^\'"]+[\'"]\s*,\s*(?:function|\()', + re.IGNORECASE, +) + +def check(code_lines, add_vulnerability): + # Track whether DRF AllowAny is imported to increase confidence + drf_allowany_seen = any(DRF_IMPORT_ALLOWANY_RE.search(line) for line in code_lines) + + # -------- Flask route without auth decorator ---------- + i = 0 + while i < len(code_lines): + line = code_lines[i] + if FLASK_ROUTE_RE.search(line): + # Collect decorators until we hit the function def line + decorators = [] + j = i + while j + 1 < len(code_lines) and not DEF_RE.search(code_lines[j + 1]): + j += 1 + if code_lines[j].lstrip().startswith('@'): + decorators.append(code_lines[j].strip()) + + # If next line is a function def, evaluate decorators + if j + 1 < len(code_lines) and DEF_RE.search(code_lines[j + 1]): + has_auth = any(AUTH_DECORATOR_RE.search(d) for d in decorators) + # Heuristic: mark as High likelihood if path looks sensitive + path_hint = "" + m = re.search(r'route\s*\(\s*[\'"]([^\'"]+)', line, re.IGNORECASE) + if m: + path_hint = m.group(1) + + if not has_auth: + sev_like = "HIGH" if re.search(r'/?(admin|settings|manage|delete|update|user|account)', path_hint, re.IGNORECASE) else "MEDIUM" + add_vulnerability( + "A02: Broken Access Control", + f"Flask route appears without an auth decorator: {line.strip()}", + i + 1, + sev_like, + "HIGH", + ) + i = j + 1 + else: + i += 1 + else: + i += 1 + + # -------- DRF AllowAny on views / viewsets ---------- + for idx, line in enumerate(code_lines): + if DRF_ALLOWANY_RE.search(line): + like = "HIGH" if drf_allowany_seen else "MEDIUM" + add_vulnerability( + "A02: Broken Access Control", + f"DRF endpoint allows unauthenticated access with AllowAny: {line.strip()}", + idx + 1, + like, + "HIGH", + ) + + # -------- Express routes without middleware ---------- + for idx, line in enumerate(code_lines): + if EXPRESS_ROUTE_RE.search(line): + add_vulnerability( + "A02: Broken Access Control", + f"Express route handler attached without visible auth middleware: {line.strip()}", + idx + 1, + "MEDIUM", + "HIGH", + ) diff --git a/scanner/rules/security_misconfiguration.py b/scanner/rules/security_misconfiguration.py new file mode 100644 index 0000000..c4893a3 --- /dev/null +++ b/scanner/rules/security_misconfiguration.py @@ -0,0 +1,86 @@ +# This module implements a check for OWASP A05: Security Misconfiguration. +# +# It flags risky configuration patterns commonly seen in Python, JS, and YAML: +# 1) Debug modes enabled (Django DEBUG=True, Flask app.run(debug=True)). +# 2) Overly permissive hosts or CORS settings (ALLOWED_HOSTS=['*'], Access-Control-Allow-Origin: *). +# 3) Insecure cookie or transport flags (SECURE_... = False, SESSION_COOKIE_SECURE=False). +# 4) Hardcoded or default-like secrets in config contexts (SECRET_KEY='...', password='admin'). +# +# Function: +# - `check(code_lines, add_vulnerability)`: Scans lines and reports findings with context. + +import re + +DJANGO_DEBUG_RE = re.compile(r'\bDEBUG\s*=\s*True\b') +FLASK_DEBUG_RE = re.compile(r'\bapp\.run\s*\(\s*.*\bdebug\s*=\s*True\b', re.IGNORECASE) +DJANGO_ALLOWED_HOSTS_ANY_RE = re.compile(r'\bALLOWED_HOSTS\s*=\s*\[\s*[\'"]\*\s*[\'"]\s*\]', re.IGNORECASE) + +CORS_WILDCARD_RE = re.compile(r'(Access-Control-Allow-Origin\s*[:=]\s*[\'"]\*\s*[\'"])|("allowAllOrigins"\s*:\s*true)', re.IGNORECASE) +SECURE_FLAG_FALSE_RE = re.compile(r'\b(SECURE_[A-Z_]+|SESSION_COOKIE_SECURE|CSRF_COOKIE_SECURE)\s*=\s*False\b') +INSECURE_COOKIE_RE = re.compile(r'cookie\s*(secure|httpOnly)\s*[:=]\s*false', re.IGNORECASE) + +DEFAULTY_SECRET_RE = re.compile( + r'\b(SECRET_KEY|APP_SECRET|JWT_SECRET|API_KEY|TOKEN|PASSWORD)\s*[:=]\s*[\'"]([^\'"]+)[\'"]', re.IGNORECASE +) +OBVIOUS_DEFAULTS = {'admin', 'password', 'changeme', 'change_me', 'default', 'test', 'secret'} + +def check(code_lines, add_vulnerability): + for i, line in enumerate(code_lines): + # Debug modes + if DJANGO_DEBUG_RE.search(line): + add_vulnerability( + "A05: Security Misconfiguration", + f"Django DEBUG is enabled: {line.strip()}", + i + 1, + "HIGH", + "MEDIUM", + ) + if FLASK_DEBUG_RE.search(line): + add_vulnerability( + "A05: Security Misconfiguration", + f"Flask debug mode is enabled: {line.strip()}", + i + 1, + "HIGH", + "MEDIUM", + ) + + # Permissive hosts and CORS + if DJANGO_ALLOWED_HOSTS_ANY_RE.search(line): + add_vulnerability( + "A05: Security Misconfiguration", + f"ALLOWED_HOSTS permits all hosts: {line.strip()}", + i + 1, + "MEDIUM", + "MEDIUM", + ) + if CORS_WILDCARD_RE.search(line): + add_vulnerability( + "A05: Security Misconfiguration", + f"Wildcard CORS detected: {line.strip()}", + i + 1, + "MEDIUM", + "MEDIUM", + ) + + # Insecure cookie and transport flags + if SECURE_FLAG_FALSE_RE.search(line) or INSECURE_COOKIE_RE.search(line): + add_vulnerability( + "A05: Security Misconfiguration", + f"Insecure cookie or transport flag: {line.strip()}", + i + 1, + "MEDIUM", + "MEDIUM", + ) + + # Default-like or hardcoded secrets + m = DEFAULTY_SECRET_RE.search(line) + if m: + key, value = m.group(1), m.group(2) + like = "HIGH" if value.strip().lower() in OBVIOUS_DEFAULTS else "MEDIUM" + add_vulnerability( + "A05: Security Misconfiguration", + f"Hardcoded secret or credential in config context: {key} = '***'", + i + 1, + like, + "HIGH", + ) diff --git a/tests/test_negative.py b/tests/test_negative.py index b767788..34f5399 100644 --- a/tests/test_negative.py +++ b/tests/test_negative.py @@ -1,11 +1,26 @@ -# This file should produce clean results - +# This file should produce clean results +import os import sqlite3 +from flask import Flask +# Assume a real auth decorator exists in the project. The scanner only checks for its presence. +def login_required(fn): # placeholder for scanning context + return fn + +# --- Secure Flask setup --- +app = Flask(__name__) +app.config["DEBUG"] = False +app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "fallback_only_for_dev_builds") + +@app.route("/dashboard") +@login_required +def dashboard(): + return "secure dashboard" +# --- Parameterised SQL query (safe) --- username = input("Enter your username: ") query = "SELECT * FROM users WHERE username = ?" conn = sqlite3.connect("example.db") cursor = conn.cursor() -cursor.execute(query, (username,)) \ No newline at end of file +cursor.execute(query, (username,)) diff --git a/tests/test_positive.py b/tests/test_positive.py index b6afb8a..eff4343 100644 --- a/tests/test_positive.py +++ b/tests/test_positive.py @@ -1,11 +1,41 @@ -# This file should trigger the SQL injection rule +# Triggers: A01 (Injection), A02 (Broken Access Control), A05 (Security Misconfiguration) import sqlite3 +from flask import Flask, Response +# ---------- A05: Security Misconfiguration ---------- +# Obvious default-like secret +SECRET_KEY = "changeme" + +# Permissive host policy +ALLOWED_HOSTS = ['*'] + +# Insecure cookie/transport flags +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False + +app = Flask(__name__) + +# ---------- A02: Broken Access Control ---------- +# Sensitive admin route with NO auth decorator present +@app.route("/admin") +def admin_panel(): + # Wildcard CORS header (also A05) + resp = Response("admin panel") + resp.headers["Access-Control-Allow-Origin"] = "*" + return resp + +# ---------- A01: Injection ---------- username = input("Enter your username: ") +# Unparameterized concatenated query assignment that starts with SELECT query = "SELECT * FROM users WHERE username = '" + username + "'" conn = sqlite3.connect("example.db") cursor = conn.cursor() -cursor.execute(query) \ No newline at end of file +# Execute the suspicious query variable +cursor.execute(query) + +# Explicit Flask debug enable (A05) +if __name__ == "__main__": + app.run(debug=True) From d3254fc534c84c690f6423d5a7b429a6402b5860 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:10:20 +1000 Subject: [PATCH 06/15] adding 2 more OWASP rules --- scanner/core.py | 14 ++++-- scanner/rules/auth_failures.py | 42 +++++++++++++++++ ...configuration.py => security_misconfig.py} | 0 scanner/rules/sensitive_data_exposure.py | 35 ++++++++++++++ tests/test_negative.py | 10 ++++ tests/test_positive.py | 47 ++++++++++++------- 6 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 scanner/rules/auth_failures.py rename scanner/rules/{security_misconfiguration.py => security_misconfig.py} (100%) create mode 100644 scanner/rules/sensitive_data_exposure.py diff --git a/scanner/core.py b/scanner/core.py index 8742ea4..2009fc9 100644 --- a/scanner/core.py +++ b/scanner/core.py @@ -6,12 +6,14 @@ # - Prints a simple report import os -from scanner.rules import sql_injection, broken_access_control, security_misconfiguration +from scanner.rules import sql_injection, broken_access_control, security_misconfig, sensitive_data_exposure, auth_failures RULE_MODULES = [ sql_injection, broken_access_control, - security_misconfiguration, + security_misconfig, + sensitive_data_exposure, + auth_failures, ] class VulnerabilityScanner: @@ -38,9 +40,11 @@ def parse_file(self): return True 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) + sql_injection.check(self.code_lines, self.add_vulnerability) + broken_access_control.check(self.code_lines, self.add_vulnerability) + security_misconfig.check(self.code_lines, self.add_vulnerability) + sensitive_data_exposure.check(self.code_lines, self.add_vulnerability) + auth_failures.check(self.code_lines, self.add_vulnerability) def run(self): if not self.parse_file(): diff --git a/scanner/rules/auth_failures.py b/scanner/rules/auth_failures.py new file mode 100644 index 0000000..c448605 --- /dev/null +++ b/scanner/rules/auth_failures.py @@ -0,0 +1,42 @@ +# Rule: A07 Identification and Authentication Failures +import re + +def check(code_lines, add_vulnerability): + for i, line in enumerate(code_lines): + # Flask/Django style routes that should require auth + if re.search(r"@app\.route\([\"'](/login|/auth|/signin)[\"']", line): + add_vulnerability( + "A07: Identification and Authentication Failures", + f"Authentication-related route without explicit auth checks: {line.strip()}", + i + 1, + "HIGH", + "MEDIUM" + ) + + # Python requests with TLS verify disabled + if "requests." in line and "verify=False" in line: + add_vulnerability( + "A07: Identification and Authentication Failures", + f"Insecure TLS verification disabled: {line.strip()}", + i + 1, + "HIGH", + "HIGH" + ) + + # Hardcoded default creds + if re.search(r"(user(name)?\s*=\s*['\"](admin|root)['\"])", line, re.IGNORECASE): + add_vulnerability( + "A07: Identification and Authentication Failures", + f"Hardcoded default username detected: {line.strip()}", + i + 1, + "HIGH", + "HIGH" + ) + if re.search(r"(password\s*=\s*['\"](admin|1234|password)['\"])", line, re.IGNORECASE): + add_vulnerability( + "A07: Identification and Authentication Failures", + f"Hardcoded default password detected: {line.strip()}", + i + 1, + "HIGH", + "HIGH" + ) diff --git a/scanner/rules/security_misconfiguration.py b/scanner/rules/security_misconfig.py similarity index 100% rename from scanner/rules/security_misconfiguration.py rename to scanner/rules/security_misconfig.py diff --git a/scanner/rules/sensitive_data_exposure.py b/scanner/rules/sensitive_data_exposure.py new file mode 100644 index 0000000..22bcac5 --- /dev/null +++ b/scanner/rules/sensitive_data_exposure.py @@ -0,0 +1,35 @@ +# Rule: A03 Sensitive Data Exposure (Cryptographic Failures) +import re + +def check(code_lines, add_vulnerability): + weak_hashes = ["md5", "sha1"] + sensitive_keywords = ["password", "passwd", "secret", "apikey", "api_key", "token"] + + for i, line in enumerate(code_lines): + stripped = line.strip() + + # Skip comments + if stripped.startswith("#"): + continue + + # Weak crypto usage + if any(h in stripped.lower() for h in weak_hashes): + add_vulnerability( + "A03: Sensitive Data Exposure", + f"Weak hashing algorithm detected: {stripped}", + i + 1, + "HIGH", + "HIGH" + ) + + # Hardcoded secrets (but ignore env lookups and hashes) + if any(kw in stripped.lower() for kw in sensitive_keywords) and "=" in stripped: + if "os.environ" in stripped or "hashlib.sha256" in stripped: + continue # safe usage, skip + add_vulnerability( + "A03: Sensitive Data Exposure", + f"Potential hardcoded sensitive data: {stripped}", + i + 1, + "HIGH", + "MEDIUM" + ) diff --git a/tests/test_negative.py b/tests/test_negative.py index 34f5399..cf6ea34 100644 --- a/tests/test_negative.py +++ b/tests/test_negative.py @@ -2,6 +2,8 @@ import os import sqlite3 +import hashlib +import requests from flask import Flask # Assume a real auth decorator exists in the project. The scanner only checks for its presence. def login_required(fn): # placeholder for scanning context @@ -24,3 +26,11 @@ def dashboard(): conn = sqlite3.connect("example.db") cursor = conn.cursor() cursor.execute(query, (username,)) + +# --- Secure cryptography usage --- +# Using a strong hashing algorithm (sha256) instead of md5/sha1 +hashed_password = hashlib.sha256(username.encode()).hexdigest() + +# --- Secure HTTP request (TLS verification enabled) --- +resp = requests.get("https://example.com", verify=True) +print(resp.status_code) diff --git a/tests/test_positive.py b/tests/test_positive.py index eff4343..4b363ae 100644 --- a/tests/test_positive.py +++ b/tests/test_positive.py @@ -1,40 +1,55 @@ -# Triggers: A01 (Injection), A02 (Broken Access Control), A05 (Security Misconfiguration) +# Triggers: +# A01 Injection +# A02 Broken Access Control +# A03 Sensitive Data Exposure (Cryptographic Failures) +# A05 Security Misconfiguration +# A07 Identification and Authentication Failures import sqlite3 +import hashlib +import requests from flask import Flask, Response # ---------- A05: Security Misconfiguration ---------- -# Obvious default-like secret -SECRET_KEY = "changeme" +SECRET_KEY = "changeme" # hardcoded secret (A05) +ALLOWED_HOSTS = ['*'] # permissive host policy (A05) +SESSION_COOKIE_SECURE = False # insecure cookie flag (A05) +CSRF_COOKIE_SECURE = False # insecure CSRF flag (A05) -# Permissive host policy -ALLOWED_HOSTS = ['*'] +# ---------- A03: Sensitive Data Exposure ---------- +password = "SuperSecret123" # potential hardcoded secret (A03) +api_key = "sk_test_123456" # potential hardcoded key (A03) +hashlib.md5(b"weak") # weak hashing algorithm (A03) -# Insecure cookie/transport flags -SESSION_COOKIE_SECURE = False -CSRF_COOKIE_SECURE = False +# ---------- A07: Identification and Authentication Failures ---------- +username = "admin" # default username (A07) +default_password = "password" # default password (A07) +requests.get("https://example.com", verify=False) # TLS verification disabled (A07) app = Flask(__name__) # ---------- A02: Broken Access Control ---------- -# Sensitive admin route with NO auth decorator present +# Sensitive route without auth decorator @app.route("/admin") def admin_panel(): - # Wildcard CORS header (also A05) + # Wildcard CORS header (A05) resp = Response("admin panel") resp.headers["Access-Control-Allow-Origin"] = "*" return resp +# Login route that should be protected or checked (A07 heuristic) +@app.route("/login") +def login_page(): + return "login page" + # ---------- A01: Injection ---------- -username = input("Enter your username: ") -# Unparameterized concatenated query assignment that starts with SELECT -query = "SELECT * FROM users WHERE username = '" + username + "'" +user_input = input("Enter your username: ") +# Unparameterized, concatenated query assignment beginning with SELECT +query = "SELECT * FROM users WHERE username = '" + user_input + "'" # A01 conn = sqlite3.connect("example.db") cursor = conn.cursor() - -# Execute the suspicious query variable -cursor.execute(query) +cursor.execute(query) # executes suspicious query var (A01) # Explicit Flask debug enable (A05) if __name__ == "__main__": From 043e902a68d98d799fa53ddb8b666e3686a40723 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:13:35 +1000 Subject: [PATCH 07/15] updated readme --- README.md | 57 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 712c441..def3dda 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,62 @@ # OWASP PR Scanner -This tool is designed to scan Python files for security vulnerabilities based on the OWASP Top 10. +This tool scans Python files for security vulnerabilities based on the **OWASP Top 10**. +It is designed for lightweight static analysis of pull requests, helping developers catch common issues early. --- ## ✅ Current Functionality -The scanner detects potential vulnerabilities using static analysis (regex and basic logic). Currently implemented: +The scanner detects potential vulnerabilities using static analysis (regex + simple logic). +Implemented rules: - **A01: Injection** - Detects unparameterized SQL queries - Flags SQL built with string concatenation or f-strings -- **Clean vs Vulnerable Detection** - - Example `test_positive.py` will trigger an alert for SQL injection - - Example `test_negative.py` is safe and produces no alerts +- **A02: Broken Access Control** + - Detects Flask routes without authentication decorators ---- - -## 🚧 Planned Features (OWASP Top 10 Coverage) -This scanner will be extended to cover the full OWASP Top 10. - -Currently implemented: +- **A03: Sensitive Data Exposure (Cryptographic Failures)** + - Detects weak hashing algorithms (e.g., MD5, SHA1) + - Flags hardcoded sensitive values (secrets, passwords, API keys) + - Warns about unsafe patterns like environment variable fallbacks if misused -✅ A01: Injection (e.g. SQL injection detection using regex) +- **A05: Security Misconfiguration** + - Detects `debug=True` in Flask apps + - Flags permissive host settings (e.g., `ALLOWED_HOSTS = ['*']`) + - Insecure cookie/CSRF flags + - Hardcoded Flask secrets -Planned: +- **A07: Identification and Authentication Failures** + - Detects default credentials (`admin`, `password`, etc.) + - Flags routes like `/login` without authentication checks + - Warns about disabled TLS verification in requests (`verify=False`) -A02: Cryptographic Failures (e.g. weak hashing, insecure SSL use) +--- -A03: Injection – more types (e.g. XSS, Command Injection) +## 📂 Test Cases -A04: Insecure Design (e.g. missing access controls) +- **`test_positive.py`** + Contains vulnerable code that should trigger A01 (SQL Injection). -A05: Security Misconfiguration (e.g. debug mode, missing headers) +- **`test_positive_all.py`** + Triggers multiple rules (A01, A02, A03, A05, A07) in one file. -A06: Vulnerable and Outdated Components (dependency scanning) +- **`test_negative.py`** + Safe code sample — should pass with **no findings** (used for regression testing). -A07: Identification and Authentication Failures (e.g. missing auth checks) +--- -A08: Software and Data Integrity Failures (e.g. unsafe deserialization) +## 🚧 Planned Features (Remaining OWASP Top 10) -A09: Security Logging and Monitoring Failures (e.g. no logging in sensitive flows) +- **A04: Insecure Design** (missing access control design patterns) +- **A06: Vulnerable and Outdated Components** (dependency scanning) +- **A08: Software and Data Integrity Failures** (e.g., unsafe deserialization) +- **A09: Security Logging and Monitoring Failures** (e.g., missing audit logging) +- **A10: Server-Side Request Forgery (SSRF)** -A10: Server-Side Request Forgery (SSRF) +--- ## Running the Script ### 1. Navigate to your project root From 8d66d24ad2c924630c8257ae69d4daf86cb30864 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:14:05 +1000 Subject: [PATCH 08/15] updated readme --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index def3dda..105a759 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,6 @@ Implemented rules: ## 📂 Test Cases - **`test_positive.py`** - Contains vulnerable code that should trigger A01 (SQL Injection). - -- **`test_positive_all.py`** Triggers multiple rules (A01, A02, A03, A05, A07) in one file. - **`test_negative.py`** From 1c1f2b3815b9fd3ba937b18469fc46f50ca2434b Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:21:35 +1000 Subject: [PATCH 09/15] cleaning up comments in test files --- tests/test_negative.py | 2 -- tests/test_positive.py | 29 ++++++++++++----------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/tests/test_negative.py b/tests/test_negative.py index cf6ea34..7bd5340 100644 --- a/tests/test_negative.py +++ b/tests/test_negative.py @@ -28,9 +28,7 @@ def dashboard(): cursor.execute(query, (username,)) # --- Secure cryptography usage --- -# Using a strong hashing algorithm (sha256) instead of md5/sha1 hashed_password = hashlib.sha256(username.encode()).hexdigest() -# --- Secure HTTP request (TLS verification enabled) --- resp = requests.get("https://example.com", verify=True) print(resp.status_code) diff --git a/tests/test_positive.py b/tests/test_positive.py index 4b363ae..3e58278 100644 --- a/tests/test_positive.py +++ b/tests/test_positive.py @@ -11,46 +11,41 @@ from flask import Flask, Response # ---------- A05: Security Misconfiguration ---------- -SECRET_KEY = "changeme" # hardcoded secret (A05) -ALLOWED_HOSTS = ['*'] # permissive host policy (A05) -SESSION_COOKIE_SECURE = False # insecure cookie flag (A05) -CSRF_COOKIE_SECURE = False # insecure CSRF flag (A05) +SECRET_KEY = "changeme" +ALLOWED_HOSTS = ['*'] +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False # ---------- A03: Sensitive Data Exposure ---------- -password = "SuperSecret123" # potential hardcoded secret (A03) -api_key = "sk_test_123456" # potential hardcoded key (A03) -hashlib.md5(b"weak") # weak hashing algorithm (A03) +password = "SuperSecret123" +api_key = "sk_test_123456" +hashlib.md5(b"weak") # ---------- A07: Identification and Authentication Failures ---------- -username = "admin" # default username (A07) -default_password = "password" # default password (A07) -requests.get("https://example.com", verify=False) # TLS verification disabled (A07) +username = "admin" +default_password = "password" +requests.get("https://example.com", verify=False) app = Flask(__name__) # ---------- A02: Broken Access Control ---------- -# Sensitive route without auth decorator @app.route("/admin") def admin_panel(): - # Wildcard CORS header (A05) resp = Response("admin panel") resp.headers["Access-Control-Allow-Origin"] = "*" return resp -# Login route that should be protected or checked (A07 heuristic) @app.route("/login") def login_page(): return "login page" # ---------- A01: Injection ---------- user_input = input("Enter your username: ") -# Unparameterized, concatenated query assignment beginning with SELECT -query = "SELECT * FROM users WHERE username = '" + user_input + "'" # A01 +query = "SELECT * FROM users WHERE username = '" + user_input + "'" conn = sqlite3.connect("example.db") cursor = conn.cursor() -cursor.execute(query) # executes suspicious query var (A01) +cursor.execute(query) -# Explicit Flask debug enable (A05) if __name__ == "__main__": app.run(debug=True) From 722365d616fa44e164343840d619bec637540de5 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:22:26 +1000 Subject: [PATCH 10/15] adding template and updating positive and negative files --- .github/workflows/scan.yml | 12 +++++++ scanner/rules/_template.py | 11 +++++++ scanner/rules/insecure_design.py | 14 +++++++++ scanner/rules/vulnerable_components.py | 15 +++++++++ tests/test_negative.py | 8 ++++- tests/test_positive.py | 43 ++++++++++++++++++-------- 6 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/scan.yml create mode 100644 scanner/rules/_template.py create mode 100644 scanner/rules/insecure_design.py create mode 100644 scanner/rules/vulnerable_components.py diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml new file mode 100644 index 0000000..bb7e688 --- /dev/null +++ b/.github/workflows/scan.yml @@ -0,0 +1,12 @@ +name: OWASP PR Scanner +on: [pull_request] +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - run: pip install -r requirements.txt + - run: python scanner/main.py tests/test_positive.py diff --git a/scanner/rules/_template.py b/scanner/rules/_template.py new file mode 100644 index 0000000..1d04ded --- /dev/null +++ b/scanner/rules/_template.py @@ -0,0 +1,11 @@ +# Template for adding new OWASP rule modules +def check(code_lines, add_vulnerability): + for i, line in enumerate(code_lines): + if "pattern" in line: # replace with real logic + add_vulnerability( + "Axx: Rule Name", + f"Description: {line.strip()}", + i + 1, + "HIGH", + "MEDIUM" + ) diff --git a/scanner/rules/insecure_design.py b/scanner/rules/insecure_design.py new file mode 100644 index 0000000..bd0fb49 --- /dev/null +++ b/scanner/rules/insecure_design.py @@ -0,0 +1,14 @@ +# OWASP A04: Insecure Design + +import re + +def check(code_lines, add_vulnerability): + for i, line in enumerate(code_lines): + if "TODO insecure" in line.lower(): + add_vulnerability( + "A04: Insecure Design", + f"Potential insecure design note found: {line.strip()}", + i + 1, + "MEDIUM", + "LOW" + ) diff --git a/scanner/rules/vulnerable_components.py b/scanner/rules/vulnerable_components.py new file mode 100644 index 0000000..1404246 --- /dev/null +++ b/scanner/rules/vulnerable_components.py @@ -0,0 +1,15 @@ +# OWASP A06: Vulnerable and Outdated Components +# Placeholder rule: looks for requirements with outdated versions. + +import re + +def check(code_lines, add_vulnerability): + for i, line in enumerate(code_lines): + if "==" in line and ("django" in line.lower() or "flask" in line.lower()): + add_vulnerability( + "A06: Vulnerable and Outdated Components", + f"Dependency pin detected (manual review required): {line.strip()}", + i + 1, + "MEDIUM", + "LOW" + ) diff --git a/tests/test_negative.py b/tests/test_negative.py index 7bd5340..61410f3 100644 --- a/tests/test_negative.py +++ b/tests/test_negative.py @@ -5,6 +5,7 @@ import hashlib import requests from flask import Flask + # Assume a real auth decorator exists in the project. The scanner only checks for its presence. def login_required(fn): # placeholder for scanning context return fn @@ -12,7 +13,7 @@ def login_required(fn): # placeholder for scanning context # --- Secure Flask setup --- app = Flask(__name__) app.config["DEBUG"] = False -app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "fallback_only_for_dev_builds") +app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "fallback_only_for_dev_builds") @app.route("/dashboard") @login_required @@ -30,5 +31,10 @@ def dashboard(): # --- Secure cryptography usage --- hashed_password = hashlib.sha256(username.encode()).hexdigest() +# --- Secure HTTP request (TLS verification enabled) --- resp = requests.get("https://example.com", verify=True) print(resp.status_code) + +# Notes: +# - No "TODO insecure" comments (A04) present. +# - No dependency pins like 'flask==...' or 'django==...' (A06) present. diff --git a/tests/test_positive.py b/tests/test_positive.py index 3e58278..000ddf8 100644 --- a/tests/test_positive.py +++ b/tests/test_positive.py @@ -1,8 +1,10 @@ -# Triggers: +# Triggers: # A01 Injection # A02 Broken Access Control # A03 Sensitive Data Exposure (Cryptographic Failures) +# A04 Insecure Design # A05 Security Misconfiguration +# A06 Vulnerable and Outdated Components # A07 Identification and Authentication Failures import sqlite3 @@ -11,41 +13,56 @@ from flask import Flask, Response # ---------- A05: Security Misconfiguration ---------- -SECRET_KEY = "changeme" -ALLOWED_HOSTS = ['*'] -SESSION_COOKIE_SECURE = False -CSRF_COOKIE_SECURE = False +SECRET_KEY = "changeme" # hardcoded secret +ALLOWED_HOSTS = ['*'] # permissive hosts +SESSION_COOKIE_SECURE = False # insecure cookie flag +CSRF_COOKIE_SECURE = False # insecure CSRF flag # ---------- A03: Sensitive Data Exposure ---------- -password = "SuperSecret123" -api_key = "sk_test_123456" -hashlib.md5(b"weak") +password = "SuperSecret123" # potential hardcoded password +api_key = "sk_test_123456" # potential hardcoded API key +hashlib.md5(b"weak") # weak hashing algorithm # ---------- A07: Identification and Authentication Failures ---------- -username = "admin" -default_password = "password" -requests.get("https://example.com", verify=False) +username = "admin" # default username +default_password = "password" # default password +requests.get("https://example.com", verify=False) # TLS verification disabled app = Flask(__name__) # ---------- A02: Broken Access Control ---------- +# Sensitive route without auth decorator @app.route("/admin") def admin_panel(): + # Wildcard CORS header (also A05) resp = Response("admin panel") resp.headers["Access-Control-Allow-Origin"] = "*" return resp +# Login route that should be protected or checked (A07 heuristic) @app.route("/login") def login_page(): return "login page" +# ---------- A04: Insecure Design ---------- +# TODO insecure: temporary admin override without proper checks + +# ---------- A06: Vulnerable and Outdated Components ---------- +# Simulate vulnerable dependency pins (scanner looks for '==' with flask/django) +requirements_block = """ +flask==0.12 +django==1.11 +""" + # ---------- A01: Injection ---------- user_input = input("Enter your username: ") -query = "SELECT * FROM users WHERE username = '" + user_input + "'" +# Unparameterized, concatenated query assignment beginning with SELECT +query = "SELECT * FROM users WHERE username = '" + user_input + "'" conn = sqlite3.connect("example.db") cursor = conn.cursor() -cursor.execute(query) +cursor.execute(query) # executes suspicious query var (A01) +# Explicit Flask debug enable (A05) if __name__ == "__main__": app.run(debug=True) From 3aaa355b538dd4b1bfce2bf719be9253a9231e93 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:24:21 +1000 Subject: [PATCH 11/15] updating core --- scanner/core.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/scanner/core.py b/scanner/core.py index 2009fc9..7c50969 100644 --- a/scanner/core.py +++ b/scanner/core.py @@ -6,7 +6,15 @@ # - Prints a simple report import os -from scanner.rules import sql_injection, broken_access_control, security_misconfig, sensitive_data_exposure, auth_failures +from scanner.rules import ( + sql_injection, + broken_access_control, + security_misconfig, + sensitive_data_exposure, + auth_failures, + insecure_design, + vulnerable_components, +) RULE_MODULES = [ sql_injection, @@ -14,6 +22,8 @@ security_misconfig, sensitive_data_exposure, auth_failures, + insecure_design, + vulnerable_components, ] class VulnerabilityScanner: @@ -40,11 +50,8 @@ def parse_file(self): return True def run_checks(self): - sql_injection.check(self.code_lines, self.add_vulnerability) - broken_access_control.check(self.code_lines, self.add_vulnerability) - security_misconfig.check(self.code_lines, self.add_vulnerability) - sensitive_data_exposure.check(self.code_lines, self.add_vulnerability) - auth_failures.check(self.code_lines, self.add_vulnerability) + for rule in RULE_MODULES: + rule.check(self.code_lines, self.add_vulnerability) def run(self): if not self.parse_file(): @@ -56,13 +63,11 @@ def report(self): # ---- colour helpers ---- def supports_truecolor() -> bool: - # Most modern terminals set COLORTERM=truecolor or 24bit return os.environ.get("COLORTERM", "").lower() in ("truecolor", "24bit") def rgb(r, g, b) -> str: return f"\033[38;2;{r};{g};{b}m" - # Fallback 8/16-colour palette ANSI = { "reset": "\033[0m", "bold": "\033[1m", "cyan": "\033[96m", "magenta": "\033[95m", From 52466b9ceeb7e325f4f62bdeb4dc7464205d290e Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:26:36 +1000 Subject: [PATCH 12/15] fixing vulnerable components --- scanner/rules/vulnerable_components.py | 26 ++++++++++++++++++++++---- tests/test_negative.py | 4 +--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/scanner/rules/vulnerable_components.py b/scanner/rules/vulnerable_components.py index 1404246..e895528 100644 --- a/scanner/rules/vulnerable_components.py +++ b/scanner/rules/vulnerable_components.py @@ -3,13 +3,31 @@ import re +# e.g., flask==2.0.1, Django==1.11.29, requests==2.25.1 +PIN_RE = re.compile(r'^\s*([A-Za-z0-9][A-Za-z0-9_\-]*)\s*==\s*([A-Za-z0-9\.\-\+]+)\s*$') + +SUSPECT_PACKAGES = {"flask", "django"} # expand as needed + def check(code_lines, add_vulnerability): for i, line in enumerate(code_lines): - if "==" in line and ("django" in line.lower() or "flask" in line.lower()): + stripped = line.strip() + + # Skip comments entirely + if stripped.startswith("#"): + continue + + m = PIN_RE.match(stripped) + if not m: + continue + + pkg = m.group(1).lower() + ver = m.group(2) + + if pkg in SUSPECT_PACKAGES: add_vulnerability( "A06: Vulnerable and Outdated Components", - f"Dependency pin detected (manual review required): {line.strip()}", + f"Dependency pin detected (manual review required): {pkg}=={ver}", i + 1, "MEDIUM", - "LOW" - ) + "LOW", + ) \ No newline at end of file diff --git a/tests/test_negative.py b/tests/test_negative.py index 61410f3..d95a0c4 100644 --- a/tests/test_negative.py +++ b/tests/test_negative.py @@ -35,6 +35,4 @@ def dashboard(): resp = requests.get("https://example.com", verify=True) print(resp.status_code) -# Notes: -# - No "TODO insecure" comments (A04) present. -# - No dependency pins like 'flask==...' or 'django==...' (A06) present. + From 673060d5ed18a8866c9753497bfcd18660c17494 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:20:18 +1000 Subject: [PATCH 13/15] adding final rules --- scanner/core.py | 122 ++++++++++++++-------------- scanner/main.py | 2 +- scanner/rules/insecure_design.py | 18 +++- scanner/rules/integrity_failures.py | 52 ++++++++++++ scanner/rules/logging_failures.py | 50 ++++++++++++ scanner/rules/ssrf.py | 39 +++++++++ tests/test_negative.py | 7 +- tests/test_positive.py | 29 ++++++- 8 files changed, 253 insertions(+), 66 deletions(-) create mode 100644 scanner/rules/integrity_failures.py create mode 100644 scanner/rules/logging_failures.py create mode 100644 scanner/rules/ssrf.py diff --git a/scanner/core.py b/scanner/core.py index 7c50969..f48deb8 100644 --- a/scanner/core.py +++ b/scanner/core.py @@ -1,31 +1,39 @@ # Responsibilities: # - Reads target file, stores code lines # - Manages vulnerability list -# - Runs all rule checks +# - Runs all rule checks (auto-discovers rules in scanner/rules) # - Provides add_vulnerability callback -# - Prints a simple report +# - Prints a grouped, colourised report import os -from scanner.rules import ( - sql_injection, - broken_access_control, - security_misconfig, - sensitive_data_exposure, - auth_failures, - insecure_design, - vulnerable_components, -) - -RULE_MODULES = [ - sql_injection, - broken_access_control, - security_misconfig, - sensitive_data_exposure, - auth_failures, - insecure_design, - vulnerable_components, -] +import importlib +import pkgutil +import scanner.rules as rules_pkg + +# -------- Rule auto-discovery -------- +def _load_rule_modules(): + modules = [] + for _, modname, _ in pkgutil.iter_modules(rules_pkg.__path__): + if modname.startswith("_"): + continue # skip __init__, _template, etc. + mod = importlib.import_module(f"{rules_pkg.__name__}.{modname}") + 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 "" + return (0, int(head[1:])) if head.startswith("A") and head[1:].isdigit() else (1, m.__name__) + + return sorted(modules, key=key) + + +RULE_MODULES = _load_rule_modules() + + +# -------- Scanner -------- class VulnerabilityScanner: def __init__(self, file_path): self.file_path = file_path @@ -33,13 +41,15 @@ def __init__(self, file_path): self.vulnerabilities = [] def add_vulnerability(self, category, description, line, severity, confidence): - self.vulnerabilities.append({ - "category": category, - "description": description, - "line": line, - "severity": severity, - "confidence": confidence, - }) + self.vulnerabilities.append( + { + "category": category, + "description": description, + "line": line, + "severity": severity, + "confidence": confidence, + } + ) def parse_file(self): if not os.path.exists(self.file_path): @@ -51,6 +61,7 @@ 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): @@ -59,8 +70,6 @@ def run(self): self.run_checks() def report(self): - import os - # ---- colour helpers ---- def supports_truecolor() -> bool: return os.environ.get("COLORTERM", "").lower() in ("truecolor", "24bit") @@ -69,31 +78,31 @@ 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": "\033[0m", + "bold": "\033[1m", + "cyan": "\033[96m", + "magenta": "\033[95m", + "yellow": "\033[93m", + "red": "\033[91m", + "green": "\033[92m", + "blue": "\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 - - sev_color = { - "CRITICAL": CRIT, - "HIGH": HIGH, - "MEDIUM": MED, - "LOW": LOW, - } + 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 + + sev_color = {"CRITICAL": CRIT, "HIGH": HIGH, "MEDIUM": MED, "LOW": LOW} print(f"\n{BOLD}{TITLE}Scan Results for {self.file_path}:{RESET}") @@ -108,7 +117,6 @@ def rgb(r, g, b) -> str: groups.setdefault(v["category"], []).append(v) def cat_key(cat: str): - # Sort A01..A10 first, then alphabetically head = cat.split(":", 1)[0].strip() return (0, int(head[1:])) if head.startswith("A") and head[1:].isdigit() else (1, cat.lower()) @@ -120,9 +128,8 @@ def cat_key(cat: str): 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}") + print(f"\n{BOLD}{HDR}=== {cat} ({total} finding{'s' if total != 1 else ''}) ==={RESET}") - # coloured summary chips chips = [] for k in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]: n = sev_counts.get(k, 0) @@ -131,12 +138,9 @@ def cat_key(cat: str): if chips: print(f"{SUM}Summary:{RESET} " + ", ".join(chips)) - # entries 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"\n {BOLD}• Line {v['line']} |{RESET} " + f"Severity {sc}{v['severity']}{RESET} | " + f"Confidence {v['confidence']}") print(f" → {v['description']}") diff --git a/scanner/main.py b/scanner/main.py index 7201f2a..8f78b47 100644 --- a/scanner/main.py +++ b/scanner/main.py @@ -5,7 +5,7 @@ import argparse -from core import VulnerabilityScanner +from scanner.core import VulnerabilityScanner def main(): diff --git a/scanner/rules/insecure_design.py b/scanner/rules/insecure_design.py index bd0fb49..8979e85 100644 --- a/scanner/rules/insecure_design.py +++ b/scanner/rules/insecure_design.py @@ -1,14 +1,24 @@ # OWASP A04: Insecure Design +# Heuristics: comments or lines that indicate insecure-by-design choices. import re +PATTERNS = [ + re.compile(r'\btodo\b.*\b(insecure|security|auth|bypass)\b', re.IGNORECASE), + re.compile(r'\btemporary\b.*\boverride\b', re.IGNORECASE), + re.compile(r'\bdisable(d)?\s+(auth(entication)?|authori[sz]ation)\b', re.IGNORECASE), + re.compile(r'\bbypass(ing)?\s+(auth|security)\b', re.IGNORECASE), +] + def check(code_lines, add_vulnerability): for i, line in enumerate(code_lines): - if "TODO insecure" in line.lower(): + stripped = line.strip() + # do NOT skip comments — we want to catch insecure design notes in comments too + if any(p.search(stripped) for p in PATTERNS): add_vulnerability( "A04: Insecure Design", - f"Potential insecure design note found: {line.strip()}", + f"Potential insecure design marker: {stripped}", i + 1, "MEDIUM", - "LOW" - ) + "LOW", + ) \ No newline at end of file diff --git a/scanner/rules/integrity_failures.py b/scanner/rules/integrity_failures.py new file mode 100644 index 0000000..1e44ffd --- /dev/null +++ b/scanner/rules/integrity_failures.py @@ -0,0 +1,52 @@ +# OWASP A08: Software and Data Integrity Failures +# Flags: eval/exec, unsafe deserialization (pickle), unsafe YAML load, and shell=True + +import re + +UNSAFE_YAML_RE = re.compile(r'\byaml\.load\s*\(') # safe form is yaml.safe_load + +def check(code_lines, add_vulnerability): + for i, line in enumerate(code_lines): + stripped = line.strip() + if stripped.startswith("#"): + continue + + # Dangerous dynamic evaluation + if "eval(" in stripped or "exec(" in stripped: + add_vulnerability( + "A08: Software and Data Integrity Failures", + f"Use of dangerous dynamic evaluation: {stripped}", + i + 1, + "HIGH", + "HIGH", + ) + + # Unsafe deserialization (pickle) + if "pickle.load(" in stripped or "pickle.loads(" in stripped: + add_vulnerability( + "A08: Software and Data Integrity Failures", + f"Potential unsafe deserialization via pickle: {stripped}", + i + 1, + "HIGH", + "HIGH", + ) + + # Unsafe YAML load (must be yaml.safe_load) + if UNSAFE_YAML_RE.search(stripped) and "safe_load" not in stripped: + add_vulnerability( + "A08: Software and Data Integrity Failures", + f"Unsafe YAML load detected; use yaml.safe_load(): {stripped}", + i + 1, + "HIGH", + "MEDIUM", + ) + + # shell=True in subprocess calls + if "subprocess." in stripped and "shell=True" in stripped: + add_vulnerability( + "A08: Software and Data Integrity Failures", + f"subprocess call with shell=True detected: {stripped}", + i + 1, + "HIGH", + "MEDIUM", + ) diff --git a/scanner/rules/logging_failures.py b/scanner/rules/logging_failures.py new file mode 100644 index 0000000..43a931b --- /dev/null +++ b/scanner/rules/logging_failures.py @@ -0,0 +1,50 @@ +# OWASP A09: Security Logging and Monitoring Failures +# Flags: printing secrets, bare except with print, and print in login/auth paths + +import re + +SECRET_WORDS = ("password", "passwd", "secret", "api_key", "apikey", "token") + +def check(code_lines, add_vulnerability): + for i, line in enumerate(code_lines): + stripped = line.strip() + low = stripped.lower() + + if stripped.startswith("#"): + continue + + # Printing potential secrets + if "print(" in low and any(w in low for w in SECRET_WORDS): + add_vulnerability( + "A09: Security Logging and Monitoring Failures", + f"Possible secret printed to stdout: {stripped}", + i + 1, + "MEDIUM", + "MEDIUM", + ) + + # Bare except printing errors (poor monitoring/alerting) + if low.startswith("except:") or re.match(r"^except\s+[A-Za-z_][A-Za-z0-9_]*\s+as\s+\w+\s*:\s*$", low): + # Peek next line(s) for print + nxt = code_lines[i + 1].strip().lower() if i + 1 < len(code_lines) else "" + if "print(" in nxt: + add_vulnerability( + "A09: Security Logging and Monitoring Failures", + f"Exception handled with print() instead of proper logging/alerting near: {stripped}", + i + 1, + "MEDIUM", + "LOW", + ) + + # Print statements in login/auth contexts (heuristic) + if ("@app.route('/login'" in low or "@app.route(\"/login\"" in low) and i + 3 < len(code_lines): + # scan a small window after the route for print usage + window = " ".join(code_lines[i : i + 5]).lower() + if "print(" in window: + add_vulnerability( + "A09: Security Logging and Monitoring Failures", + "Print used in authentication flow; prefer structured, secure logging.", + i + 1, + "MEDIUM", + "LOW", + ) diff --git a/scanner/rules/ssrf.py b/scanner/rules/ssrf.py new file mode 100644 index 0000000..c64fa35 --- /dev/null +++ b/scanner/rules/ssrf.py @@ -0,0 +1,39 @@ +# OWASP A10: Server-Side Request Forgery (SSRF) +# Heuristic data-flow: user input -> variable -> requests.*(var) + +import re + +REQUEST_CALL_RE = re.compile(r'\brequests\.(get|post|put|patch|delete|head)\s*\(') + +def check(code_lines, add_vulnerability): + input_vars = set() + + # Track variables that come from input() + for i, line in enumerate(code_lines): + stripped = line.strip() + if stripped.startswith("#"): + continue + + # var = input("...") + m = re.match(r'^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*input\s*\(', stripped) + if m: + input_vars.add(m.group(1)) + + # Flag when those variables are used in requests.*(var) + for i, line in enumerate(code_lines): + stripped = line.strip() + if stripped.startswith("#"): + continue + + if REQUEST_CALL_RE.search(stripped): + # naive arg capture + for var in input_vars: + if re.search(rf'\b{var}\b', stripped): + add_vulnerability( + "A10: Server-Side Request Forgery", + f"Potential SSRF: unvalidated user-controlled URL passed to requests.*(): {stripped}", + i + 1, + "HIGH", + "HIGH", + ) + break diff --git a/tests/test_negative.py b/tests/test_negative.py index d95a0c4..06ac430 100644 --- a/tests/test_negative.py +++ b/tests/test_negative.py @@ -7,7 +7,7 @@ from flask import Flask # Assume a real auth decorator exists in the project. The scanner only checks for its presence. -def login_required(fn): # placeholder for scanning context +def login_required(fn): return fn # --- Secure Flask setup --- @@ -36,3 +36,8 @@ def dashboard(): print(resp.status_code) +# --- Safe YAML load --- +data = yaml.safe_load("key: value") + +# --- Safe subprocess usage (no shell=True) --- +subprocess.run(["echo", "hello"], check=True) \ No newline at end of file diff --git a/tests/test_positive.py b/tests/test_positive.py index 000ddf8..7bb30b1 100644 --- a/tests/test_positive.py +++ b/tests/test_positive.py @@ -6,10 +6,16 @@ # A05 Security Misconfiguration # A06 Vulnerable and Outdated Components # A07 Identification and Authentication Failures +# A08 Software and Data Integrity Failures +# A09 Security Logging and Monitoring Failures +# A10 Server-Side Request Forgery (SSRF) import sqlite3 import hashlib import requests +import yaml +import pickle +import subprocess from flask import Flask, Response # ---------- A05: Security Misconfiguration ---------- @@ -42,18 +48,39 @@ def admin_panel(): # Login route that should be protected or checked (A07 heuristic) @app.route("/login") def login_page(): + print("login attempt for user") # A09: print in auth flow return "login page" # ---------- A04: Insecure Design ---------- # TODO insecure: temporary admin override without proper checks # ---------- A06: Vulnerable and Outdated Components ---------- -# Simulate vulnerable dependency pins (scanner looks for '==' with flask/django) +# Simulated vulnerable pins inside code string (still scanned by our rule) requirements_block = """ flask==0.12 django==1.11 """ +# ---------- A08: Software and Data Integrity Failures ---------- +user_code = "1 + 2" +result = eval(user_code) # dangerous dynamic evaluation +data = yaml.load("key: value") # unsafe YAML load (should be yaml.safe_load) +with open("tmp.bin", "wb") as fh: + pickle.dump({"x": 1}, fh) # create a pickle to then load (unsafe) +with open("tmp.bin", "rb") as fh: + obj = pickle.load(fh) # unsafe deserialization +subprocess.run("echo hi", shell=True) # shell=True + +# ---------- A09: Security Logging and Monitoring Failures ---------- +try: + raise ValueError("x") +except: + print("error:", default_password) # prints secret-ish value and uses bare-except + +# ---------- A10: SSRF ---------- +url = input("Enter URL: ") +requests.get(url) # user-controlled URL + # ---------- A01: Injection ---------- user_input = input("Enter your username: ") # Unparameterized, concatenated query assignment beginning with SELECT From 12c74dcf85bb687dbf3bf787161a8bd096f96e30 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:23:19 +1000 Subject: [PATCH 14/15] final readme update --- README.md | 86 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 105a759..0570292 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,87 @@ # OWASP PR Scanner This tool scans Python files for security vulnerabilities based on the **OWASP Top 10**. -It is designed for lightweight static analysis of pull requests, helping developers catch common issues early. +It is designed for lightweight static analysis of pull requests, helping developers catch common issues early and enforce secure coding practices. --- ## ✅ Current Functionality -The scanner detects potential vulnerabilities using static analysis (regex + simple logic). +The scanner detects vulnerabilities using static analysis (regex + simple heuristics). +It groups results by OWASP Top 10 category and highlights severity with colour-coded output. + Implemented rules: -- **A01: Injection** - - Detects unparameterized SQL queries - - Flags SQL built with string concatenation or f-strings +- **A01: Injection** + - Detects unparameterized SQL queries + - Flags SQL built with string concatenation or f-strings + +- **A02: Broken Access Control** + - Detects Flask routes without authentication decorators + +- **A03: Sensitive Data Exposure (Cryptographic Failures)** + - Detects weak hashing algorithms (MD5, SHA1) + - Flags hardcoded secrets, API keys, and default passwords + - Warns about unsafe fallback values + +- **A04: Insecure Design** + - Flags insecure “TODO” markers, temporary overrides, or auth bypass notes + +- **A05: Security Misconfiguration** + - Detects `debug=True` in Flask apps + - Flags permissive host settings (`ALLOWED_HOSTS = ['*']`) + - Insecure cookie/CSRF flags + - Hardcoded Flask secrets -- **A02: Broken Access Control** - - Detects Flask routes without authentication decorators +- **A06: Vulnerable and Outdated Components** + - Detects dependency pins like `flask==0.12` or `django==1.11` + - Helps identify outdated or risky components -- **A03: Sensitive Data Exposure (Cryptographic Failures)** - - Detects weak hashing algorithms (e.g., MD5, SHA1) - - Flags hardcoded sensitive values (secrets, passwords, API keys) - - Warns about unsafe patterns like environment variable fallbacks if misused +- **A07: Identification and Authentication Failures** + - Detects default credentials (`admin`, `password`) + - Flags login routes without auth checks + - Warns about disabled TLS verification (`verify=False`) -- **A05: Security Misconfiguration** - - Detects `debug=True` in Flask apps - - Flags permissive host settings (e.g., `ALLOWED_HOSTS = ['*']`) - - Insecure cookie/CSRF flags - - Hardcoded Flask secrets +- **A08: Software and Data Integrity Failures** + - Detects dangerous use of `eval()` + - Warns about unsafe deserialization (`pickle.load`) + - Flags subprocess calls with `shell=True` -- **A07: Identification and Authentication Failures** - - Detects default credentials (`admin`, `password`, etc.) - - Flags routes like `/login` without authentication checks - - Warns about disabled TLS verification in requests (`verify=False`) +- **A09: Security Logging and Monitoring Failures** + - Detects print statements in auth flows + - Flags bare `except:` blocks with no logging + - Warns when secrets are printed to stdout + +- **A10: Server-Side Request Forgery (SSRF)** + - Detects unvalidated user input passed into `requests.get/post` --- ## 📂 Test Cases - **`test_positive.py`** - Triggers multiple rules (A01, A02, A03, A05, A07) in one file. + A deliberately vulnerable file that triggers all implemented OWASP rules (A01–A10). - **`test_negative.py`** - Safe code sample — should pass with **no findings** (used for regression testing). + A safe baseline file with secure practices — should pass with **no findings**. + Used for regression testing and validation. --- -## 🚧 Planned Features (Remaining OWASP Top 10) +## 🎨 Output Example -- **A04: Insecure Design** (missing access control design patterns) -- **A06: Vulnerable and Outdated Components** (dependency scanning) -- **A08: Software and Data Integrity Failures** (e.g., unsafe deserialization) -- **A09: Security Logging and Monitoring Failures** (e.g., missing audit logging) -- **A10: Server-Side Request Forgery (SSRF)** +- Findings are grouped by OWASP category (A01–A10) +- Severity levels are **colour-coded**: + - 🔴 High + - 🟠 Medium + - 🟢 Low + +Example: +=== A01: Injection (2 findings) === +Summary: High: 2 + +• Line 60 | Severity HIGH | Confidence MEDIUM +→ SQL query created via string concatenation: ... --- From 3ab8d43a8e273f1586da8242fe6518de839d74d0 Mon Sep 17 00:00:00 2001 From: Liana Perry <62174756+lperry022@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:32:26 +1000 Subject: [PATCH 15/15] updated naming of rules to match redback documentation on OWASP --- README.md | 27 ++++++++++++------------ scanner/rules/auth_failures.py | 5 ++++- scanner/rules/broken_access_control.py | 2 +- scanner/rules/insecure_design.py | 5 +++-- scanner/rules/integrity_failures.py | 2 +- scanner/rules/logging_failures.py | 2 +- scanner/rules/security_misconfig.py | 2 +- scanner/rules/sensitive_data_exposure.py | 5 ++++- scanner/rules/sql_injection.py | 2 +- scanner/rules/ssrf.py | 2 +- scanner/rules/vulnerable_components.py | 2 +- 11 files changed, 32 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 0570292..b42465f 100644 --- a/README.md +++ b/README.md @@ -12,51 +12,52 @@ It groups results by OWASP Top 10 category and highlights severity with colour-c Implemented rules: -- **A01: Injection** - - Detects unparameterized SQL queries - - Flags SQL built with string concatenation or f-strings - -- **A02: Broken Access Control** +- **A01:2021 – Broken Access Control** - Detects Flask routes without authentication decorators -- **A03: Sensitive Data Exposure (Cryptographic Failures)** +- **A02:2021 – Cryptographic Failures** - Detects weak hashing algorithms (MD5, SHA1) - Flags hardcoded secrets, API keys, and default passwords - Warns about unsafe fallback values -- **A04: Insecure Design** +- **A03:2021 – Injection** + - Detects unparameterized SQL queries + - Flags SQL built with string concatenation or f-strings + +- **A04:2021 – Insecure Design** - Flags insecure “TODO” markers, temporary overrides, or auth bypass notes -- **A05: Security Misconfiguration** +- **A05:2021 – Security Misconfiguration** - Detects `debug=True` in Flask apps - Flags permissive host settings (`ALLOWED_HOSTS = ['*']`) - Insecure cookie/CSRF flags - Hardcoded Flask secrets -- **A06: Vulnerable and Outdated Components** +- **A06:2021 – Vulnerable and Outdated Components** - Detects dependency pins like `flask==0.12` or `django==1.11` - Helps identify outdated or risky components -- **A07: Identification and Authentication Failures** +- **A07:2021 – Identification and Authentication Failures** - Detects default credentials (`admin`, `password`) - Flags login routes without auth checks - Warns about disabled TLS verification (`verify=False`) -- **A08: Software and Data Integrity Failures** +- **A08:2021 – Software and Data Integrity Failures** - Detects dangerous use of `eval()` - Warns about unsafe deserialization (`pickle.load`) - Flags subprocess calls with `shell=True` -- **A09: Security Logging and Monitoring Failures** +- **A09:2021 – Security Logging and Monitoring Failures** - Detects print statements in auth flows - Flags bare `except:` blocks with no logging - Warns when secrets are printed to stdout -- **A10: Server-Side Request Forgery (SSRF)** +- **A10:2021 – Server-Side Request Forgery (SSRF)** - Detects unvalidated user input passed into `requests.get/post` --- + ## 📂 Test Cases - **`test_positive.py`** diff --git a/scanner/rules/auth_failures.py b/scanner/rules/auth_failures.py index c448605..faacceb 100644 --- a/scanner/rules/auth_failures.py +++ b/scanner/rules/auth_failures.py @@ -1,4 +1,7 @@ -# Rule: A07 Identification and Authentication Failures +# A07:2021 – Identification and Authentication Failures +# Detects default credentials (`admin`, `password`) +# Flags login routes without auth checks +# Warns about disabled TLS verification (`verify=False`) import re def check(code_lines, add_vulnerability): diff --git a/scanner/rules/broken_access_control.py b/scanner/rules/broken_access_control.py index 2897c04..2038991 100644 --- a/scanner/rules/broken_access_control.py +++ b/scanner/rules/broken_access_control.py @@ -1,4 +1,4 @@ -# This module implements a check for OWASP A02: Broken Access Control. +# A01:2021 – Broken Access Control # # It looks for common patterns that suggest missing or weak authorization checks: # 1) Flask routes without an auth/role decorator (e.g., @login_required, @jwt_required). diff --git a/scanner/rules/insecure_design.py b/scanner/rules/insecure_design.py index 8979e85..5548256 100644 --- a/scanner/rules/insecure_design.py +++ b/scanner/rules/insecure_design.py @@ -1,5 +1,6 @@ -# OWASP A04: Insecure Design -# Heuristics: comments or lines that indicate insecure-by-design choices. +# A04:2021 – Insecure Design +# Flags insecure “TODO” markers, temporary overrides, or auth bypass notes + import re diff --git a/scanner/rules/integrity_failures.py b/scanner/rules/integrity_failures.py index 1e44ffd..b9a29c9 100644 --- a/scanner/rules/integrity_failures.py +++ b/scanner/rules/integrity_failures.py @@ -1,4 +1,4 @@ -# OWASP A08: Software and Data Integrity Failures +# A08:2021 – Software and Data Integrity Failure # Flags: eval/exec, unsafe deserialization (pickle), unsafe YAML load, and shell=True import re diff --git a/scanner/rules/logging_failures.py b/scanner/rules/logging_failures.py index 43a931b..dbe62d8 100644 --- a/scanner/rules/logging_failures.py +++ b/scanner/rules/logging_failures.py @@ -1,4 +1,4 @@ -# OWASP A09: Security Logging and Monitoring Failures +# A09:2021 – Security Logging and Monitoring Failures # Flags: printing secrets, bare except with print, and print in login/auth paths import re diff --git a/scanner/rules/security_misconfig.py b/scanner/rules/security_misconfig.py index c4893a3..17885d2 100644 --- a/scanner/rules/security_misconfig.py +++ b/scanner/rules/security_misconfig.py @@ -1,4 +1,4 @@ -# This module implements a check for OWASP A05: Security Misconfiguration. +# A05:2021 – Security Misconfiguration # # It flags risky configuration patterns commonly seen in Python, JS, and YAML: # 1) Debug modes enabled (Django DEBUG=True, Flask app.run(debug=True)). diff --git a/scanner/rules/sensitive_data_exposure.py b/scanner/rules/sensitive_data_exposure.py index 22bcac5..c914ea7 100644 --- a/scanner/rules/sensitive_data_exposure.py +++ b/scanner/rules/sensitive_data_exposure.py @@ -1,4 +1,7 @@ -# Rule: A03 Sensitive Data Exposure (Cryptographic Failures) +# A02:2021 – Cryptographic Failures +# Detects weak hashing algorithms (MD5, SHA1) +# Flags hardcoded secrets, API keys, and default passwords +# Warns about unsafe fallback values import re def check(code_lines, add_vulnerability): diff --git a/scanner/rules/sql_injection.py b/scanner/rules/sql_injection.py index 519e4bb..a220d9c 100644 --- a/scanner/rules/sql_injection.py +++ b/scanner/rules/sql_injection.py @@ -1,4 +1,4 @@ -# This module implements a check for OWASP A01: Injection vulnerabilities. +# A03:2021 – Injection* # Specifically, it searches for suspicious SQL query patterns in Python code, # such as unparameterized queries or string concatenation in `execute()` calls. diff --git a/scanner/rules/ssrf.py b/scanner/rules/ssrf.py index c64fa35..c5cb654 100644 --- a/scanner/rules/ssrf.py +++ b/scanner/rules/ssrf.py @@ -1,4 +1,4 @@ -# OWASP A10: Server-Side Request Forgery (SSRF) +# A10:2021 – Server-Side Request Forgery (SSRF) # Heuristic data-flow: user input -> variable -> requests.*(var) import re diff --git a/scanner/rules/vulnerable_components.py b/scanner/rules/vulnerable_components.py index e895528..a7c0288 100644 --- a/scanner/rules/vulnerable_components.py +++ b/scanner/rules/vulnerable_components.py @@ -1,4 +1,4 @@ -# OWASP A06: Vulnerable and Outdated Components +# A06:2021 – Vulnerable and Outdated Components # Placeholder rule: looks for requirements with outdated versions. import re