I Built a Penetration Testing Tool From Scratch - Here's What I Learned
By N00dleN00b | WaspSting v1.5 | Open Source | Apache 2.0
The Problem Nobody Talks About in Bug Bounty
Everyone talks about finding bugs. Nobody talks about the hours you spend after you find one.
I'm a cybersecurity student at The University of Tulsa, and like a lot of people in this field, I got hooked on ethical hacking through TryHackMe, HackTheBox, and Burp Suite Labs. The labs are great because it's controlled, structured, and satisfying. You find the vulnerability, you get the flag, you move on. Clean.
Then I tried my first real bug bounty hunt on Bugcrowd.
The actual discovery part? Exciting. Everything else? A grind. I was manually keeping track of endpoints in a notepad, copy-pasting evidence into a Word document, writing up findings one by one, trying to remember which OWASP category applied to what, and losing hours to documentation that had nothing to do with actually finding bugs.
I wanted to get straight to the meat: discovery, exploitation, and verification. Not writing up that yes, the X-Frame-Options header was missing, for the fifteenth time.
So I built WaspSting.
What is WaspSting?
WaspSting is a Python CLI tool for authorized penetration testing and bug bounty hunting. The core idea is simple: the documentation should write itself.
You run a scan. WaspSting makes the requests, identifies the issues, maps them to OWASP Top 10:2025, calculates CVSS v3.1 scores, and generates a structured Markdown and JSON report automatically. By the time you're done testing, your report is already 80% written.
It's built entirely in Python, uses zero paid APIs, and has only two dependencies: rich for terminal output and requests for HTTP. Everything else is either standard library or optional external tools.
git clone https://github.com/N00dleN00b/WaspSting.git
cd WaspSting
pip install -r requirements.txt
python3 waspsting.py --help
How It Works: A Technical Deep Dive
WaspSting is built around a modular architecture. Each testing capability lives in its own module under modules/, and the main waspsting.py orchestrates them based on the mode you select.
Scan Modes
full — All modules at once
sast — Static analysis of a GitHub repo
recon — Security headers, tech fingerprinting, CVE lookup
auth — Login lockout testing, JWT attack documentation
bola — BOLA/IDOR sequential ID walking
api — Rate limiting, CORS, injection probes
enum — Subdomain enumeration via crt.sh + DNS brute force
fuzz — Payload fuzzer across 9 vulnerability categories
nuclei — Nuclei template runner
bounty — Scope ingestion → phased test plan
The Findings Pipeline
Every module returns a standardised findings dict:
{
"module": "recon",
"owasp_id": "A02",
"owasp_name": "Security Misconfiguration",
"severity": "HIGH",
"title": "Missing Content-Security-Policy",
"description": "CSP missing — XSS risk increased",
"evidence": "Header absent from response",
"fix": "Add Content-Security-Policy header",
"cvss_score": 6.5,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
}
This means every finding, regardless of which module produced it, flows through the same reporter, gets scored by the same CVSS engine, and ends up in the same structured report.
CVSS v3.1 Scoring: Pure Python, No Library
One of the features I'm most proud of is the CVSS calculator. Rather than pulling in an external library, I implemented the full CVSS v3.1 base score formula from scratch in modules/cvss.py.
The spec defines a specific formula with metric weights for Attack Vector, Attack Complexity, Privileges Required, User Interaction, Scope, and the CIA triad (Confidentiality, Integrity, Availability). The tricky part is the "round up" rule CVSS uses ceiling rounding to one decimal place, not standard rounding.
def calculate_score(vector: CVSSVector) -> float:
iss = 1 - (1 - c) * (1 - i) * (1 - a)
if vector.S == "U":
impact = 6.42 * iss
else:
impact = 7.52 * (iss - 0.029) - 3.25 * ((iss - 0.02) ** 15)
exploitability = 8.22 * av * ac * pr * ui
raw = min(impact + exploitability, 10) if vector.S == "U" \
else min(1.08 * (impact + exploitability), 10)
return math.ceil(raw * 10) / 10 # CVSS ceiling rounding
Every finding is auto-mapped to a vector based on its OWASP category and title. A SQL injection maps to AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (network reachable, no privileges, scope changed, full CIA impact) — a 10.0 CRITICAL. A missing header maps to something lower. You can also override any finding's vector interactively with --cvss-override.
The Fuzzer — And Why False Positives Were a Problem
The fuzzer (modules/fuzzer.py) sends payloads across the following 9 categories: SQLi, XSS, SSTI, path traversal, SSRF, command injection, NoSQL, prompt injection, and open redirect.
Early on I had a serious false-positive problem. When testing against OWASP Juice Shop (a React SPA), every single XSS and SSTI payload was flagging as a hit. The tool was reporting 60+ HIGH findings that were all fake.
The root cause: I was checking if the payload appeared anywhere in the response body, but React SPAs return the same index.html shell for every request regardless of query parameters. The payload appeared in the URL, which appeared in the response, which triggered the match.
The fix was a two-part SPA guard:
def _is_spa_shell(response_text: str) -> bool:
indicators = ["<app-root>", "<div id=\"root\">", "data-beasties-container"]
return any(ind.lower() in response_text.lower() for ind in indicators)
# If baseline is SPA and response is same size/status — skip it
same_response = (r.status_code == baseline_status and size_diff < 50)
if baseline_is_spa and same_response:
continue
And tighter detection signatures (SSTI) only triggers if 49 appears as a standalone token (the evaluated result of {{7*7}}), not just anywhere in the response. XSS only triggers if the tag appears unencoded in the body.
Custom OWASP Pattern Rules
v1.5 adds a YAML-based rules engine that lets you write your own detection patterns:
- id: aws-key-exposure
name: AWS Access Key Exposed
owasp: A02
severity: CRITICAL
match:
response_body: "AKIA[0-9A-Z]{16}"
source_code: "(AKIA|ASIA|AROA)[0-9A-Z]{16}"
fix: Rotate the key immediately and audit CloudTrail.
Rules can match against HTTP response bodies, source code files (SAST mode), header presence/absence, header values, and status codes. Drop your YAML files in rules/ and they're picked up automatically.
Bugcrowd API Integration
Instead of manually copying scope from a Bugcrowd program page, WaspSting can pull it directly:
export BUGCROWD_API_TOKEN=your_token_here
python3 waspsting.py --bugcrowd-program acme-corp
It fetches the target groups, filters to web-relevant targets, converts wildcards (*.example.com) to their apex domain with a clear warning, and saves a WaspSting-compatible scope JSON ready to pass to --mode bounty.
Finding Real Bugs — DVWA Walkthrough
Let's put it into practice against DVWA (Damn Vulnerable Web Application), a deliberately vulnerable app you can run locally for safe, legal testing.
Setup
# Run DVWA in Docker
docker run -d -p 8080:80 vulnerables/web-dvwa
Open http://localhost:8080, log in with admin/password, click Setup/Reset DB, then set security level to Low in the DVWA Security tab.
Step 1 — Recon
python3 waspsting.py --target http://localhost:8080 --mode recon --rules --confirm
What you'll see: Missing security headers (HSTS, CSP, X-Frame-Options), no HTTPS, CVSS scores auto-populated for each finding.
Step 2 — Auth Audit
python3 waspsting.py --target http://localhost:8080 --mode auth --confirm
What you'll see: WaspSting finds the login endpoint, tests lockout policy (DVWA has none on Low security), documents JWT attack vectors for manual testing.
Step 3 — Fuzzer
python3 waspsting.py --target http://localhost:8080 --mode fuzz --confirm
What you'll see: On DVWA at Low security, the fuzzer should trigger on SQL injection endpoints because DVWA actually returns database errors and real signatures, not SPA noise.
Step 4 — Full Scan with HTML Report
python3 waspsting.py \
--target http://localhost:8080 \
--mode full \
--html --rules --cvss-override \
--confirm
What you'll see: Every module runs sequentially, findings accumulate, CVSS table prints at the end, HTML report saved to output/.
Open the HTML report in your browser and that's your executive report with risk gauge, severity charts, and filterable findings table.
What I Learned as Building a Security Tool
1. Building tools forces you to understand the underlying concepts deeply
You can't write a CVSS calculator without understanding how Attack Vector, Scope, and the CIA triad interact. You can't write a fuzzer without understanding why SPAs behave differently from server-rendered apps. The act of building forced me to go deeper than any lab ever did.
2. False positives are a real problem and most write-ups skip it
Every security tool deals with false positives. The difference between a useful tool and a noisy one is how carefully you tune detection. I spent more time on the SPA guard than on the entire fuzzer payload list.
3. Good architecture matters more than features
The decision to standardize all findings into the same dict format early on meant that CVSS scoring, deduplication, notifications, and reporting all just worked when I added them and no refactoring needed. The modular design meant I could add Nuclei integration without touching any existing module.
4. Documentation is part of the product
The README, the example rules file, the clear error messages are as important as the code. A tool with great features and terrible docs gets ignored. A tool with decent features and great docs gets used and contributed to.
5. Real-world testing is humbling
The moment I ran WaspSting against Juice Shop and saw 60 false-positive XSS findings, I understood something that no CTF ever taught me: the difference between "payload sent" and "vulnerability confirmed" is everything. That lesson shapes how I think about every finding now.
How to Use WaspSting for Bug Bounty
Here's the workflow I'd recommend for a typical Bugcrowd engagement:
1. Import scope
export BUGCROWD_API_TOKEN=your_token
python3 waspsting.py --bugcrowd-program target-program
2. Run the bounty planner
python3 waspsting.py --mode bounty --scope output/scope_target_SESSION.json
This gives you a phased test plan prioritised by payout potential.
3. Recon each in-scope target
python3 waspsting.py --target https://app.target.com --mode recon --cve --rules --confirm
4. Subdomain enumeration
python3 waspsting.py --target https://target.com --mode enum --screenshot --confirm
5. Full scan on interesting subdomains
python3 waspsting.py --target https://app.target.com --mode full \
--html --rules --burp --dedup --confirm
6. Manual follow-up
Use the generated Burp config to load scope and payloads into Burp Suite Community for manual testing of anything the automated scan flagged.
7. Clear session when done
python3 clear_session.py
Saves your reports, wipes the output directory, and you're ready for the next program.
What's Next
The roadmap still has some exciting items:
- HackerOne API integration (same as Bugcrowd but H1's OAuth flow)
- CVSS v4.0 support (the spec dropped in 2023, still catching up)
- Custom OWASP pattern sharing — a community rules repository
- CI/CD integration docs for teams running WaspSting in pipelines
- General optimization, fine-tuning for accuracy, and I'm sure there's more things I haven't even considered yet.
If you find a bug, want to contribute a module, or just want to say the tool helped you find something on a real program, open an issue or a PR. I'm learning as I go and every contribution teaches me something.
Get Started
git clone https://github.com/N00dleN00b/WaspSting.git
cd WaspSting
pip install -r requirements.txt
python3 waspsting.py --help
Licensed Apache 2.0. Free to use, fork, and build on.
If WaspSting helped you find a bug — star the repo. It means a lot as a student building in public.
N00dleN00b is a cybersecurity student at TU passionate about ethical hacking, bug bounty research, and building open-source security tooling.
GitHub: github.com/N00dleN00b/WaspSting


















Top comments (0)