diff --git a/.github/workflows/websecscan.yml b/.github/workflows/websecscan.yml
new file mode 100644
index 0000000..713ebcf
--- /dev/null
+++ b/.github/workflows/websecscan.yml
@@ -0,0 +1,71 @@
+name: WebSecScan
+
+on:
+ schedule:
+ - cron: "0 3 * * *" # daily at 03:00 UTC
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+concurrency:
+ group: websecscan
+ cancel-in-progress: true
+
+jobs:
+ scan:
+ runs-on: ubuntu-latest
+ env:
+ VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install WebSecScan
+ run: |
+ python -m pip install --upgrade pip
+ pip install .
+
+ - name: Run scans
+ run: |
+ mkdir -p reports
+ TS=$(date -u +%Y%m%d_%H%M%S)
+ if [ -f targets.txt ]; then
+ while IFS= read -r url; do
+ [ -z "$url" ] && continue
+ websecscan "$url" --output html --fast || true
+ dom=$(echo "$url" | sed 's|https\?://||; s|/||g')
+ if [ -f "reports/scan_${dom}.html" ]; then
+ mv "reports/scan_${dom}.html" "reports/${TS}_${dom}.html"
+ fi
+ done < targets.txt
+ else
+ echo "No targets.txt found; skipping scans."
+ fi
+ # Disable Jekyll on GitHub Pages
+ touch reports/.nojekyll
+
+ # Generate a simple index.html listing all report files
+ echo "
WebSecScan ReportsWebSecScan Reports
Generated by GitHub Actions
" > reports/index.html
+ for f in $(ls -1t reports/*.html 2>/dev/null | sed 's|reports/||' | grep -v '^index.html$'); do
+ echo "- $f
" >> reports/index.html
+ done
+ echo "
To run a new scan, go to Actions and trigger the WebSecScan workflow.
" >> reports/index.html
+
+ - name: Upload artifact (reports)
+ uses: actions/upload-artifact@v4
+ with:
+ name: reports-${{ github.run_number }}
+ path: reports/
+
+ - name: Publish to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_branch: gh-pages
+ publish_dir: reports
+ keep_files: true
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..5e91c3e
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+recursive-include scanner/templates *
diff --git a/README.md b/README.md
index fda9c7c..80e5f91 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,114 @@ python3 websecscan.py https://example.com --output md --active
---
+## 🚀 Run on GitHub (scheduled, auto-published reports)
+
+Follow these steps to host the project on GitHub and keep it running on a schedule via GitHub Actions. Reports will be committed to a `gh-pages` branch and published with GitHub Pages.
+
+### 1) Push this project to your GitHub repo
+- Create a new repository on GitHub (public or private)
+- From your local machine or CI environment:
+```bash
+git init
+git add .
+git commit -m "feat: initial import of WebSecScan"
+git branch -M main
+git remote add origin git@github.com:/.git
+git push -u origin main
+```
+
+### 2) Add your VirusTotal API key as a secret
+- GitHub → Your repository → Settings → Secrets and variables → Actions → New repository secret
+- Name: `VIRUSTOTAL_API_KEY`
+- Value: your actual API key
+
+### 3) Add a `targets.txt` file (domains you own)
+- Create a file named `targets.txt` in the repo root containing one authorized URL per line, for example:
+```text
+https://example.com
+https://sub.example.com
+```
+
+### 4) Add the GitHub Actions workflow
+- Create the file `.github/workflows/websecscan.yml` with the following content:
+```yaml
+name: WebSecScan
+
+on:
+ schedule:
+ - cron: "0 3 * * *" # daily at 03:00 UTC
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+concurrency:
+ group: websecscan
+ cancel-in-progress: true
+
+jobs:
+ scan:
+ runs-on: ubuntu-latest
+ env:
+ VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install WebSecScan
+ run: |
+ python -m pip install --upgrade pip
+ pip install .
+
+ - name: Run scans
+ run: |
+ mkdir -p reports
+ TS=$(date -u +%Y%m%d_%H%M%S)
+ while IFS= read -r url; do
+ [ -z "$url" ] && continue
+ websecscan "$url" --output html --fast || true
+ dom=$(echo "$url" | sed 's|https\?://||; s|/||g')
+ if [ -f "reports/scan_${dom}.html" ]; then
+ mv "reports/scan_${dom}.html" "reports/${TS}_${dom}.html"
+ fi
+ done < targets.txt
+
+ - name: Upload artifact (reports)
+ uses: actions/upload-artifact@v4
+ with:
+ name: reports-${{ github.run_number }}
+ path: reports/
+
+ - name: Publish to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_branch: gh-pages
+ publish_dir: reports
+ keep_files: true
+```
+
+### 5) Enable GitHub Pages
+- Repo → Settings → Pages → Build and deployment
+- Source: `Deploy from a branch`
+- Branch: `gh-pages`, Folder: `/ (root)`
+- Save. Your reports will be available at `https://.github.io//`
+
+### 6) Run it now
+- Go to Actions → `WebSecScan` → Run workflow
+- After it finishes, view the Pages site or download the artifact
+
+### Notes and tips
+- Only include domains you own or are authorized to scan in `targets.txt`.
+- Remove `--fast` in the workflow to enable all checks (slower, more API use).
+- To keep history, the workflow timestamps report filenames; adjust as needed.
+- If you need private reports, skip GitHub Pages and rely on build artifacts only.
+- For heavy scans or tighter control, consider a VPS + `cron`/`systemd` instead of Actions.
+
## 🤝 Contributing
Pull requests, feature suggestions, and plugins are welcome!
Feel free to fork the project and submit your ideas, as I will be checking the project from time to time.
@@ -118,4 +226,4 @@ Built for security researchers, red teamers, and educators.
## ⚠️ Disclaimer
Use responsibly.
This tool is intended for **authorized testing only**.
-Do not scan websites without permission.
+Do not scan websites without permission.
\ No newline at end of file
diff --git a/reports/scan_example.com.md b/reports/scan_example.com.md
new file mode 100644
index 0000000..ea79fee
--- /dev/null
+++ b/reports/scan_example.com.md
@@ -0,0 +1,76 @@
+# 🛡️ WebSecScan Report
+
+**Target:** https://example.com
+**Scan Time:** 2025-10-27 18:28:40.472899
+
+---
+
+## 🔐 SSL Certificate
+
+- **issuer:** ((('countryName', 'US'),), (('organizationName', 'DigiCert Inc'),), (('commonName', 'DigiCert Global G3 TLS ECC SHA384 2020 CA1'),))
+
+- **subject:** ((('countryName', 'US'),), (('stateOrProvinceName', 'California'),), (('localityName', 'Los Angeles'),), (('organizationName', 'Internet Corporation for Assigned Names and Numbers'),), (('commonName', '*.example.com'),))
+
+- **valid_from:** Jan 15 00:00:00 2025 GMT
+
+- **valid_until:** Jan 15 23:59:59 2026 GMT
+
+
+---
+
+## 🧱 WAF Detection
+**WAF:** None detected
+
+---
+
+## 📑 Security Headers
+
+- **Content-Security-Policy:** Missing
+
+- **X-Content-Type-Options:** Missing
+
+- **Strict-Transport-Security:** Missing
+
+- **X-Frame-Options:** Missing
+
+
+---
+
+## 🧬 Tech Stack
+
+- None detected
+
+
+---
+
+## 📌 CVE Matches
+
+- None
+
+
+---
+
+## 📁 Sensitive Paths Found
+
+- None
+
+
+---
+
+## 📊 Risk Score
+**Score:** 25/100
+
+- ⚠️ Missing headers: Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security, X-Frame-Options
+
+- ⚠️ No WAF detected
+
+
+---
+
+## 🔗 Links Analyzed
+
+### https://iana.org/domains/example
+
+- **IP:** 192.0.43.8
+- **WHOIS:**
+- **Heuristics:**
diff --git a/requirements.txt b/requirements.txt
index b0f63e5..cbbbf33 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,4 +8,5 @@ python-whois>=0.8.0
Jinja2>=3.1.2
aiodns>=3.0.0
scikit-learn>1.3.0
-joblib>=1.3.2
\ No newline at end of file
+joblib>=1.3.2
+scikit-learn
\ No newline at end of file
diff --git a/scanner/__pycache__/__init__.cpython-313.pyc b/scanner/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..7189496
Binary files /dev/null and b/scanner/__pycache__/__init__.cpython-313.pyc differ
diff --git a/scanner/__pycache__/active_tester.cpython-313.pyc b/scanner/__pycache__/active_tester.cpython-313.pyc
new file mode 100644
index 0000000..e853682
Binary files /dev/null and b/scanner/__pycache__/active_tester.cpython-313.pyc differ
diff --git a/scanner/__pycache__/anti_bot.cpython-313.pyc b/scanner/__pycache__/anti_bot.cpython-313.pyc
new file mode 100644
index 0000000..208dded
Binary files /dev/null and b/scanner/__pycache__/anti_bot.cpython-313.pyc differ
diff --git a/scanner/__pycache__/async_tools.cpython-313.pyc b/scanner/__pycache__/async_tools.cpython-313.pyc
new file mode 100644
index 0000000..fd06994
Binary files /dev/null and b/scanner/__pycache__/async_tools.cpython-313.pyc differ
diff --git a/scanner/__pycache__/auth_scanner.cpython-313.pyc b/scanner/__pycache__/auth_scanner.cpython-313.pyc
new file mode 100644
index 0000000..abe04bc
Binary files /dev/null and b/scanner/__pycache__/auth_scanner.cpython-313.pyc differ
diff --git a/scanner/__pycache__/core.cpython-313.pyc b/scanner/__pycache__/core.cpython-313.pyc
new file mode 100644
index 0000000..de77e57
Binary files /dev/null and b/scanner/__pycache__/core.cpython-313.pyc differ
diff --git a/scanner/__pycache__/dir_brute.cpython-313.pyc b/scanner/__pycache__/dir_brute.cpython-313.pyc
new file mode 100644
index 0000000..ef64984
Binary files /dev/null and b/scanner/__pycache__/dir_brute.cpython-313.pyc differ
diff --git a/scanner/__pycache__/extract.cpython-313.pyc b/scanner/__pycache__/extract.cpython-313.pyc
new file mode 100644
index 0000000..3bb2a5d
Binary files /dev/null and b/scanner/__pycache__/extract.cpython-313.pyc differ
diff --git a/scanner/__pycache__/headers.cpython-313.pyc b/scanner/__pycache__/headers.cpython-313.pyc
new file mode 100644
index 0000000..074f04a
Binary files /dev/null and b/scanner/__pycache__/headers.cpython-313.pyc differ
diff --git a/scanner/__pycache__/heuristics.cpython-313.pyc b/scanner/__pycache__/heuristics.cpython-313.pyc
new file mode 100644
index 0000000..debe8a6
Binary files /dev/null and b/scanner/__pycache__/heuristics.cpython-313.pyc differ
diff --git a/scanner/__pycache__/ip_tools.cpython-313.pyc b/scanner/__pycache__/ip_tools.cpython-313.pyc
new file mode 100644
index 0000000..613c1c6
Binary files /dev/null and b/scanner/__pycache__/ip_tools.cpython-313.pyc differ
diff --git a/scanner/__pycache__/profiling.cpython-313.pyc b/scanner/__pycache__/profiling.cpython-313.pyc
new file mode 100644
index 0000000..8d5d8b8
Binary files /dev/null and b/scanner/__pycache__/profiling.cpython-313.pyc differ
diff --git a/scanner/__pycache__/recon.cpython-313.pyc b/scanner/__pycache__/recon.cpython-313.pyc
new file mode 100644
index 0000000..e2b97a2
Binary files /dev/null and b/scanner/__pycache__/recon.cpython-313.pyc differ
diff --git a/scanner/__pycache__/report.cpython-313.pyc b/scanner/__pycache__/report.cpython-313.pyc
new file mode 100644
index 0000000..d348b92
Binary files /dev/null and b/scanner/__pycache__/report.cpython-313.pyc differ
diff --git a/scanner/__pycache__/smart_detect.cpython-313.pyc b/scanner/__pycache__/smart_detect.cpython-313.pyc
new file mode 100644
index 0000000..efec3a4
Binary files /dev/null and b/scanner/__pycache__/smart_detect.cpython-313.pyc differ
diff --git a/scanner/__pycache__/spider.cpython-313.pyc b/scanner/__pycache__/spider.cpython-313.pyc
new file mode 100644
index 0000000..d1b7b71
Binary files /dev/null and b/scanner/__pycache__/spider.cpython-313.pyc differ
diff --git a/scanner/__pycache__/ssl_analyzer.cpython-313.pyc b/scanner/__pycache__/ssl_analyzer.cpython-313.pyc
new file mode 100644
index 0000000..2cc65ec
Binary files /dev/null and b/scanner/__pycache__/ssl_analyzer.cpython-313.pyc differ
diff --git a/scanner/__pycache__/stack_fingerprint.cpython-313.pyc b/scanner/__pycache__/stack_fingerprint.cpython-313.pyc
new file mode 100644
index 0000000..5fce76e
Binary files /dev/null and b/scanner/__pycache__/stack_fingerprint.cpython-313.pyc differ
diff --git a/scanner/__pycache__/subdomains.cpython-313.pyc b/scanner/__pycache__/subdomains.cpython-313.pyc
new file mode 100644
index 0000000..22dd65b
Binary files /dev/null and b/scanner/__pycache__/subdomains.cpython-313.pyc differ
diff --git a/scanner/__pycache__/virustotal.cpython-313.pyc b/scanner/__pycache__/virustotal.cpython-313.pyc
new file mode 100644
index 0000000..4b341b5
Binary files /dev/null and b/scanner/__pycache__/virustotal.cpython-313.pyc differ
diff --git a/scanner/__pycache__/vuln_check.cpython-313.pyc b/scanner/__pycache__/vuln_check.cpython-313.pyc
new file mode 100644
index 0000000..b46fcee
Binary files /dev/null and b/scanner/__pycache__/vuln_check.cpython-313.pyc differ
diff --git a/scanner/async_tools.py b/scanner/async_tools.py
index dd191b3..1dac8ce 100644
--- a/scanner/async_tools.py
+++ b/scanner/async_tools.py
@@ -52,7 +52,7 @@ def load_plugins(plugin_folder="plugins"):
spec.loader.exec_module(mod)
if hasattr(mod, "run_plugin"):
plugins.append(mod)
- return plugins
+ return plugins
def run_plugins(plugins, url, html, headers):
results = []
diff --git a/scanner/core.py b/scanner/core.py
index 1b5034e..10acb75 100644
--- a/scanner/core.py
+++ b/scanner/core.py
@@ -140,9 +140,9 @@ def scan_website(url, fast_mode=False, use_plugins=False, output_format="json",
soup = BeautifulSoup(resp.text, 'html.parser')
for script in soup.find_all('script'):
if script.string:
- js_snippets.appeend(script.string)
+ js_snippets.append(script.string)
report["suspicious_analysis"] = is_url_suspicious(link, js_snippets)
- except:
+ except Exception:
report["suspicious_analysis"] = {"error": "Failed to analyze scripts"}
@@ -157,10 +157,12 @@ def scan_website(url, fast_mode=False, use_plugins=False, output_format="json",
report["ip"] = ip
report["host_profile"] = get_host_profile(ip)
- except:
+ except Exception:
report["ip"] = None
report["host_profile"] = {"error": "Could not resolve IP"}
- scan_summary["reports"].append(report)
+
+ # Always record the report for this link
+ scan_summary["reports"].append(report)
# Final risk score
scan_summary["risk"] = calculate_risk_score(scan_summary)
diff --git a/scanner/extract.py b/scanner/extract.py
index 68dfb28..abb2ea8 100644
--- a/scanner/extract.py
+++ b/scanner/extract.py
@@ -16,5 +16,5 @@ def extract_links(url):
def detect_ip_grabber(url):
import re
patterns = [r'\d{10,}', r'[a-zA-Z]{20,}', r'[0-9a-f]{32}']
- domains = ["grabify.link", "iplogger.org", "ipgrabber.io", "grabify.link"]
+ domains = ["grabify.link", "iplogger.org", "ipgrabber.io"]
return any(re.search(p, url) for p in patterns) or any(d in url for d in domains)
diff --git a/scanner/headers.py b/scanner/headers.py
index f01aab3..ad9a3e7 100644
--- a/scanner/headers.py
+++ b/scanner/headers.py
@@ -8,5 +8,5 @@ def analyze_headers(headers):
return {h: headers.get(h, "Missing") for h in important}
def detect_waf(headers):
- waf_keys = ["Server", "X-CDN", "CF-RAY", "X-Sucuro-ID", "X-Akamai"]
+ waf_keys = ["Server", "X-CDN", "CF-RAY", "X-Sucuri-ID", "X-Akamai"]
return {k: headers[k] for k in waf_keys if k in headers}
diff --git a/scanner/profiling.py b/scanner/profiling.py
index 0f3dc9f..f884378 100644
--- a/scanner/profiling.py
+++ b/scanner/profiling.py
@@ -99,7 +99,7 @@ def calculate_risk_score(scan_summary):
if missing:
penalties.append(f"Missing headers: {', '.join(missing)}")
- if scan_summary.get("waf") in ["None", "", None]:
+ if not scan_summary.get("waf"):
score += 5
penalties.append("No WAF detected")
@@ -120,5 +120,4 @@ def calculate_risk_score(scan_summary):
risk_score = min(score, 100)
- return {"risk_score": risk_score, "penalties": penalties}
-
+ return {"risk_score": risk_score, "penalties": penalties}
\ No newline at end of file
diff --git a/scanner/report.py b/scanner/report.py
index b24d291..fbe8238 100644
--- a/scanner/report.py
+++ b/scanner/report.py
@@ -1,27 +1,35 @@
import os
import json
from jinja2 import Environment, FileSystemLoader
+from pathlib import Path
-template_loader = FileSystemLoader('./templates')
-env = Environment(loader=template_loader)
-
-# Setup Jinja2 environment
-template_loader = FileSystemLoader("./templates")
+# Setup Jinja2 environment once, resolving templates in-package
+PACKAGE_DIR = Path(__file__).resolve().parent
+DEFAULT_TEMPLATE_DIR = PACKAGE_DIR / "templates"
+template_loader = FileSystemLoader(str(DEFAULT_TEMPLATE_DIR))
env = Environment(loader=template_loader)
def export_html_report(scan_data, output_path):
try:
+ # Ensure output directory exists
+ output_dir = os.path.dirname(output_path) or "."
+ os.makedirs(output_dir, exist_ok=True)
+
template = env.get_template("report_template.html")
rendered = template.render(scan=scan_data)
with open(output_path, "w", encoding="utf-8") as f:
f.write(rendered)
return True
except Exception as e:
- return f"HTML export failded: {e}"
+ return f"HTML export failed: {e}"
def export_md_report(scan_data, output_path):
try:
+ # Ensure output directory exists
+ output_dir = os.path.dirname(output_path) or "."
+ os.makedirs(output_dir, exist_ok=True)
+
template = env.get_template("report_template.md")
rendered = template.render(scan=scan_data)
with open(output_path, "w", encoding="utf-8") as f:
@@ -31,7 +39,12 @@ def export_md_report(scan_data, output_path):
return f"Markdown export failed: {e}"
def save_scan_report(data, filename):
- os.makedirs("reports", exist_ok=True)
- with open(os.path.join("reports", filename), "w") as f:
+ """Save JSON report to the provided path.
+
+ If 'filename' includes directories, they will be created automatically.
+ """
+ output_dir = os.path.dirname(filename) or "."
+ os.makedirs(output_dir, exist_ok=True)
+ with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
- print(f"✅ Saved to reports/{filename}")
\ No newline at end of file
+ print(f"✅ Saved to {filename}")
\ No newline at end of file
diff --git a/templates/report_template.html b/scanner/templates/report_template.html
similarity index 73%
rename from templates/report_template.html
rename to scanner/templates/report_template.html
index 9f6b5eb..5e7eac9 100644
--- a/templates/report_template.html
+++ b/scanner/templates/report_template.html
@@ -37,23 +37,35 @@ 📑 Security Headers
🧬 Technology Stack
- {% for tech in scan.tech_stack %}
- - {{ tech }}
- {% endfor %}
+ {% if scan.tech_stack is defined and scan.tech_stack %}
+ {% for tech in scan.tech_stack %}
+ - {{ tech }}
+ {% endfor %}
+ {% else %}
+ - None detected
+ {% endif %}
📌 CVE Matches
- {% for k, v in scan.cve_matches.items() %}
- - {{ k }}: {{ v | join(", ") }}
- {% endfor %}
+ {% if scan.cve_matches is defined and scan.cve_matches %}
+ {% for k, v in scan.cve_matches.items() %}
+ - {{ k }}: {{ v | join(", ") }}
+ {% endfor %}
+ {% else %}
+ - None
+ {% endif %}
📁 Sensitive Paths
- {% for path in scan.dir_brute %}
- - {{ path }}
- {% endfor %}
+ {% if scan.dir_brute is defined and scan.dir_brute %}
+ {% for path in scan.dir_brute %}
+ - {{ path }}
+ {% endfor %}
+ {% else %}
+ - None
+ {% endif %}
📊 Risk Score
@@ -77,4 +89,4 @@ 🔗 Links Analyzed
{% endfor %}