Konstas.Engineering Notes
Networking · Cybersecurity · Infrastructure
Defensive Security

An OWASP Top 10 File Scanner with Live Malware Lookups

Articles · September 2026 · Ioannis (John) Konstas

Summary. This note walks through a small, self-hosted scanner that does two jobs at once: it matches source files against an OWASP Top 10 pattern engine, and it checks each file’s hash against four free online reputation databases — VirusTotal, CIRCL hashlookup, MalwareBazaar, and the NIST NVD. Everything runs on the laptop behind an HTTPS localhost dashboard; nothing but a SHA-256 hash and a package name ever leaves the machine. The code is on GitHub, and the detection patterns and lookup logic are reproduced below.
OWASP Top 10 VirusTotal CIRCL hashlookup MalwareBazaar NIST NVD Flask HTTPS

1. Two questions, one pass over the file

Most laptop scanners answer only one of two questions. A linter or SAST tool asks “does this code contain a risky pattern?” An antivirus engine asks “does this file match a known-bad signature?” This scanner asks both on the same pass, because the answers are complementary: a pattern match tells you the code you wrote is dangerous, and a hash match tells you the file you received is dangerous.

Pattern engineOnline reputation
InputThe file’s text contentThe file’s SHA-256 hash (and package names)
QuestionDoes this code introduce an OWASP Top 10 weakness?Has this exact file been seen as malware?
RunsFully offline, on-deviceSends only the hash to public APIs
OutputSeverity-ranked findings by OWASP categoryDetection ratios and known-sample flags

2. The OWASP Top 10 pattern engine

The engine is a table of (category, severity, label, regex) rows — one or more per OWASP 2021 category. Keeping the rules as data rather than code means a new detection is one line, and the same table drives both the Bash terminal scanner and the Python dashboard. A representative slice:

Python — detection rules (excerpt)
# (OWASP ID, severity, label, regex)
PATTERNS = [
    ("A01", "HIGH",     "Path traversal",            r"\.\./\.\./"),
    ("A02", "CRITICAL", "Hardcoded password",        r"password\s*=\s*[\"'][^\"']{4,}[\"']"),
    ("A02", "CRITICAL", "Hardcoded secret or API key", r"(secret|api_key)\s*=\s*[\"'][^\"']{4,}[\"']"),
    ("A03", "CRITICAL", "SQL injection sink",        r"execute\(.*%.*\)"),
    ("A04", "MEDIUM",   "Insecure randomness",       r"random\.random\("),
    ("A05", "CRITICAL", "AWS secret key",           r"(aws_secret|AWS_SECRET)\s*="),
    ("A07", "HIGH",     "Hardcoded bearer token",   r"Bearer [A-Za-z0-9_.~+/-]{20,}"),
    ("A08", "HIGH",     "Unsafe deserialization",   r"pickle\.loads?\("),
    ("A09", "MEDIUM",   "Password/secret in log",   r"(log|print|logger)\s*.*(password|secret|api_key)"),
    ("A10", "HIGH",     "SSRF / metadata endpoint", r"169\.254\.169\.254"),
]

def scan_text(text: str):
    """Yield one finding per matching line, ordered as the file reads."""
    for lineno, line in enumerate(text.splitlines(), 1):
        for owasp_id, severity, label, rx in PATTERNS:
            if re.search(rx, line):
                yield {"id": owasp_id, "severity": severity, "label": label, "line": lineno}

Full OWASP 2021 coverage in the scanner spans all ten categories:

IDCategoryExamples detected
A01Broken Access ControlAdmin bypass, path traversal, IDOR
A02Cryptographic FailuresHardcoded secrets, weak hash (MD5/SHA1), private keys
A03InjectionSQL, command, LDAP, template, prompt injection
A04Insecure DesignDebug mode, insecure randomness
A05Security MisconfigurationAWS keys, wildcard CORS, disabled SSL
A06Vulnerable ComponentsOutdated packages, unsafe pickle
A07Auth FailuresJWT none alg, hardcoded tokens, insecure cookies
A08Software & Data IntegrityUnvalidated deserialization, eval on external data
A09Logging FailuresPasswords/secrets in logs
A10SSRFUser-controlled URLs, metadata endpoint access
Why regex, and where it stops. Line-level patterns are fast, dependency-free, and easy to audit — ideal for a live file watcher. They are deliberately not a replacement for a data-flow SAST engine: a regex sees execute("... %s ...") but cannot prove the argument is attacker-controlled. Treat findings as “look here,” not “proven exploitable.”

3. Four reputation databases, hash-only

The second half of a scan is reputation. Every file is hashed once with SHA-256, and that hash — never the file — is checked against four services. Three need no API key at all; only VirusTotal requires one, and its free tier is generous (500 lookups/day).

DatabaseWhat it answersKey required
VirusTotalFile hash vs 70+ AV enginesYes (free tier: 500/day)
CIRCL hashlookupIs this hash known-good or known-bad?No
MalwareBazaarIs this hash a catalogued malware sample?No
NIST NVDKnown CVEs for a package nameNo

The VirusTotal lookup is a single authenticated GET keyed on the hash. The response’s last_analysis_stats gives the detection ratio directly:

Python — VirusTotal hash lookup
import os, hashlib, urllib.request, urllib.error, json

def sha256_file(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()

def virustotal(sha256: str):
    vt_key = os.environ.get("VIRUSTOTAL_API_KEY")
    if not vt_key:
        return {"source": "VIRUSTOTAL", "status": "UNKNOWN",
                "detail": "Set VIRUSTOTAL_API_KEY env var"}
    req = urllib.request.Request(
        f"https://www.virustotal.com/api/v3/files/{sha256}",
        headers={"x-apikey": vt_key, "User-Agent": "owasp-scanner/2.0"})
    try:
        data = json.loads(urllib.request.urlopen(req, timeout=15).read())
        stats = data["data"]["attributes"]["last_analysis_stats"]
        hits, total = stats["malicious"], sum(stats.values())
        status = "MALICIOUS" if hits else "CLEAN"
        return {"source": "VIRUSTOTAL", "status": status, "detail": f"{hits}/{total} engines"}
    except urllib.error.HTTPError as e:
        if e.code == 404:                         # hash never submitted = not known-bad
            return {"source": "VIRUSTOTAL", "status": "UNKNOWN", "detail": "not in dataset"}
        raise
A 404 is not “clean.” If VirusTotal has never seen the hash it returns 404 — which means unknown, not safe. The scanner reports that as UNKNOWN rather than a green tick, so a novel sample is never mislabelled as harmless. CIRCL and MalwareBazaar are queried the same hash-only way, and the NVD lookup takes a package name via keywordSearch to surface known CVEs for a dependency.

4. The HTTPS localhost dashboard

The dashboard is a small Flask app that serves the same engine over a browser UI. It binds to localhost only and speaks TLS with a self-signed certificate, so scan results and any pasted content never cross the network in cleartext — even though they never leave the machine either. Findings are ranked by severity, and each file row carries its four reputation verdicts.

Python — Flask over TLS, localhost only
import os
from flask import Flask, request, jsonify

SSL_CERT = os.path.expanduser("~/.owasp_ssl/cert.pem")
SSL_KEY  = os.path.expanduser("~/.owasp_ssl/key.pem")

app = Flask(__name__)

@app.route("/scan", methods=["POST"])
def scan():
    text = request.get_json(force=True).get("content", "")
    findings = sorted(scan_text(text),
                      key=lambda f: ["CRITICAL", "HIGH", "MEDIUM", "LOW"].index(f["severity"]))
    return jsonify(findings=findings)

if __name__ == "__main__":
    # bind to loopback only; never 0.0.0.0
    app.run(host="127.0.0.1", port=8443, ssl_context=(SSL_CERT, SSL_KEY))

5. Setup and daily use

Two dependencies, one self-signed certificate, and an optional VirusTotal key:

Shell — one-time setup
brew install fswatch
pip3 install flask

# self-signed cert for the localhost dashboard
mkdir -p ~/.owasp_ssl
openssl req -x509 -newkey rsa:4096 \
  -keyout ~/.owasp_ssl/key.pem \
  -out ~/.owasp_ssl/cert.pem \
  -days 365 -nodes -subj "/CN=localhost"

# optional: enable VirusTotal lookups
export VIRUSTOTAL_API_KEY=your_key_here

The Bash side installs as shell functions, so scanning is part of the prompt rather than a separate tool to remember:

Shell — terminal scanner
source owasp_terminal_prompt.sh

oscan <file>       # scan a single file
odir  <path>       # scan all code under a directory
ogit  [repo]       # scan only git-changed files
owatch             # live watcher — auto-scans new files (fswatch)
olog               # show recent findings

# HTTPS dashboard
python3 owasp_dashboard.py   # then open https://localhost:8443

The owatch mode is the one that changes habits: fswatch feeds new and changed files into the engine as they land, so a secret pasted into a config or a malicious dependency dropped into a project is flagged at write time rather than at some later audit.

6. What it is and is not

References

Research and direction by Ioannis (John) Konstas; compiled and finalized with AI assistance.

← All articles

👁· views