Automated security scanner for open source GitHub repositories. Runs independent checks — VirusTotal, Semgrep, Trivy + Grype (deduplicated CVE scanning), and Hadolint — entirely via Docker. No local installations required beyond Docker, Git, curl, and jq.
Generates a self-contained, interactive HTML report (report.html) plus machine-readable report.json, with a color-coded verdict (PASS / WARN / FAIL) per scanner and an overall risk score.
| Scanner | What it checks |
|---|---|
| 🦠 VirusTotal | Malware detection across 70+ AV engines (via vt-cli); cache-first with automatic upload and 5-minute analysis timeout |
| 🔎 Semgrep | Static code analysis — security patterns, bad practices; errors surfaced in terminal summary |
| 🛡️ Trivy | CVE vulnerabilities in dependencies, secrets in source code, misconfigs |
| 📦 Grype | Second CVE engine — cross-checks dependencies against a different advisory database for broader coverage |
| 🐳 Hadolint | Dockerfile best-practices and security linting |
- Scan any public GitHub repository by URL
- Pin to a specific release tag or branch with
--release - Trivy + Grype dual CVE scanning — findings are deduplicated on a canonical key and each row is labelled with its source (
Trivy,Grype, orTrivy + Grypewhen both engines agree); GHSA ids from Grype are aliased to their CVE so the two engines reconcile - Overall risk score plus per-scanner PASS / WARN / FAIL verdicts
- Diff mode (
--compare DIR) — compare against a prior report and surface what changed - Per-repo config & cache — repeat scans are served from cache;
--forceruns fresh - Skip individual scanners:
--no-vt,--no-semgrep,--no-trivy,--no-grype,--no-hadolint - Interactive HTML report with light/dark theme toggle and a fluid layout that fills any screen width (375px – 2560px)
- Report language: English and Russian (
--lang ru) - VirusTotal detail view — file metadata (SHA256/MD5/SHA1, size, type) + per-engine malicious detections
- Semgrep severity mapping — ERROR → HIGH, WARNING → MEDIUM (no confusion with script errors)
- Clickable GitHub links in findings tables — jump directly to the vulnerable line
- Report is always generated — even if a scanner fails mid-run
- Docker (daemon must be running)
git,curl,jq- VirusTotal API key (free tier: 4 req/min) — optional, VT scan is skipped without it
git clone https://github.com/DreDvil/oss-security-scanner.git
cd oss-security-scanner
chmod +x check.sh
# Optional: add your VirusTotal API key
cp .env.example .env
echo "VT_API_KEY=your_key_here" >> .envcheck.sh # entry point — run ./check.sh <repo-url>
lib/
scanner.merge.jq # Trivy + Grype dedup / merge (canonical-key grouping)
scanner.grype-normalize.jq # Grype → common schema (GHSA→CVE aliasing)
docker/
Dockerfile.vt # vt-cli:local image (VirusTotal CLI)
# Basic scan (default branch HEAD)
./check.sh https://github.com/owner/repo
# Scan a specific release tag
./check.sh https://github.com/owner/repo --release v2.4.0
# Compare against a previous report (diff mode)
./check.sh https://github.com/owner/repo --compare reports/20260101_120000_owner_repo
# Force a fresh scan, bypassing the cache
./check.sh https://github.com/owner/repo --force
# Report in Russian
./check.sh https://github.com/owner/repo --lang ru
# Skip VirusTotal (no API key)
./check.sh https://github.com/owner/repo --no-vt
# Pass VT key inline
VT_API_KEY=xxx ./check.sh https://github.com/sigstore/cosign --release v2.2.4
# Skip multiple scanners
./check.sh https://github.com/owner/repo --no-vt --no-trivy| Flag | Description |
|---|---|
--release TAG |
Scan a specific tag or branch (default: HEAD) |
--lang LANG |
Report language: en (default) or ru |
--compare DIR |
Compare against a prior report directory (adds a diff section) |
--force |
Bypass the scan cache and run a full fresh scan |
--semgrep-min-severity LEVEL |
Minimum Semgrep severity to show: info|low|medium|high (default: medium) |
--no-vt |
Skip VirusTotal scan |
--no-semgrep |
Skip Semgrep scan |
--no-trivy |
Skip Trivy scan |
--no-grype |
Skip Grype scan |
--no-hadolint |
Skip Hadolint Dockerfile scan |
--vt-key KEY |
VirusTotal API key (overrides .env) |
| Variable | Description |
|---|---|
VT_API_KEY |
VirusTotal API key — can also be set in .env |
LANG_REPORT |
Report language (en or ru) |
check.sh https://github.com/owner/repo
│
├─ git clone --depth 1 (or --branch TAG)
│
├─ 🦠 VirusTotal
│ └─ tar + sha256 → vt-cli (Docker, built from docker/Dockerfile.vt)
│ ├─ Cache hit → fetch existing analysis
│ └─ Cache miss → upload file, wait for results (5 min timeout)
│ └─ Report: file metadata + per-engine malicious detections
│
├─ 🔎 Semgrep
│ └─ docker run semgrep/semgrep
│ ├─ Level 1: --config=auto (language-aware)
│ ├─ Level 2: language-specific rulesets (js, py, go, java …)
│ └─ Level 3: bundled p/default (always runs as fallback)
│
├─ 🛡️ Trivy + 📦 Grype
│ ├─ docker run aquasec/trivy fs --scanners vuln,secret,misconfig
│ ├─ docker run anchore/grype dir:. (second CVE engine)
│ └─ merge → dedup on canonical key (id+pkg+version), GHSA→CVE aliased,
│ each finding labelled Trivy / Grype / Trivy + Grype
│
├─ 🐳 Hadolint
│ └─ docker run hadolint/hadolint (all Dockerfiles in repo)
│
├─ 🧮 Risk score + per-scanner verdicts
│
└─ 📄 Generate reports/.../report.html + report.json
| Status | Condition |
|---|---|
| ✅ PASS | No critical issues across all scanners |
| Non-critical findings (HIGH CVEs, Semgrep warnings, Hadolint warnings) | |
| ❌ FAIL | Malware detected or CRITICAL CVEs or secrets in source |
The repository ships two offline regression suites that cover the Trivy + Grype
merge/dedup logic and the Grype diagnostics layer. They run entirely offline
(no Docker, no network) and require only jq and bash:
bash tests/merge_vulns.test.sh # Trivy + Grype merge / dedup (canonical-key grouping)
bash tests/grype_diag.test.sh # Grype package counts, DB-age, inconclusive triggersEach suite prints a per-assertion PASS/FAIL line and a final tally, exiting
non-zero if any assertion fails.
reports/
└── 20260319_195100_owner_repo/
├── report.html ← main report (open in browser)
├── report.json ← machine-readable data (jq-queryable)
├── raw/ ← scanner data artifacts (linked from report)
│ ├── semgrep.json, trivy_fs.json, grype.json, hadolint.json
│ ├── vulns_merged.json, virustotal.txt, …
└── logs/ ← run diagnostics (internal; not linked)
├── semgrep.log (scanner stderr), semgrep-run.log, trivy-run.log, …
└── semgrep.env, trivy.env, grype.env, …
Open the report:
open reports/*/report.html # macOS
xdg-open reports/*/report.html # Linux- Light & dark themes — toggle in the header, preference persisted across runs; module cards per scanner
- Dependencies table (Trivy + Grype): one deduplicated CVE list with a
Sourcecolumn (Trivy/Grype/Trivy + Grype), severity, CVSS, fixed-in version, and a diagnostics line when Grype is inconclusive - VirusTotal block: file hashes (SHA256/MD5/SHA1), size, type, reputation, scan dates + per-engine malicious verdict table
- Semgrep table: numbered rows, severity mapped to HIGH/MEDIUM/LOW/INFO, clickable GitHub links to exact lines
- Trivy secrets: file links pointing directly to the affected file in the repo
- Hadolint: lists all Dockerfile issues with rule codes, severity, and line numbers
- Fluid layout — fills any screen width from 375px to 2560px; long file paths truncate with a hover tooltip, long messages wrap
- Accessibility — keyboard focus indicators on all interactive elements; external links include
rel="noopener noreferrer" - XSS-safe — all scanner output (file paths, rule IDs, detection names) is set via DOM
textContent(neverinnerHTML)
| Image | Dockerfile | Purpose |
|---|---|---|
vt-cli:local |
docker/Dockerfile.vt |
VirusTotal CLI (Go binary); base images digest-pinned for supply-chain safety |
All images are built automatically on first use (~1–3 min each) and cached by Docker for subsequent runs. hadolint/hadolint and semgrep/semgrep are pulled from Docker Hub.
- VirusTotal free tier: 4 requests/min, 500 req/day. Large repos (>650 MB archive) are skipped automatically.
- VirusTotal upload: repos not found in VT's cache are uploaded automatically and analysed. Upload + analysis has a 5-minute ceiling; if VT is slow, the scan is skipped with an
errorbadge (not a false pass). - VirusTotal freshness: each run downloads a fresh archive from GitHub. If the SHA256 differs from a previous upload, VT treats it as a new file and starts a fresh scan — some engines may time out on the first run. Results stabilise if the same file is re-checked.
- Semgrep: uses a 3-level fallback (auto → language-specific rulesets → bundled p/default) to maximise rule coverage regardless of network availability.
- Hadolint: automatically finds all
Dockerfile*files in the repository. Skipped (SKIP badge) if none are found. - Trivy + Grype: both fetch their vulnerability databases on each run. They use different advisory sources, so coverage differs per ecosystem (e.g. Trivy parses
bun.lock, which Grype does not yet). The report dedups overlapping findings and labels each row by source. - Grype "inconclusive": in directory mode Grype only sees dependencies it can pin from committed lockfiles/manifests. A repo with no resolvable manifests yields 0 Grype findings — the report flags this as inconclusive rather than a clean pass.
- All scanners run independently — a failure in one does not stop others.
- Saving a PDF: open
report.htmlin a browser and use Print → Save as PDF — the report ships a print stylesheet for clean output.
This project is licensed under the MIT License © 2026 Danil Grechishkin — see the LICENSE file for the full text.