Skip to content

Commit a5bb78f

Browse files
authored
Merge pull request #691 from blacklanternsecurity/dev
Dev->Main
2 parents 63c0f2d + 5fced63 commit a5bb78f

82 files changed

Lines changed: 6312 additions & 729 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/read-sources.yml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ jobs:
1313
readsources:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v3
16+
- uses: actions/checkout@v4
17+
with:
18+
ref: dev
1719

1820
- name: Set up Python
1921
uses: actions/setup-python@v4
@@ -118,10 +120,6 @@ jobs:
118120
continue
119121
fi
120122
121-
# Add the new hash to the history file
122-
echo "adding hash $new_hash to signature file" >> readsources_action.log
123-
echo "$new_hash #$file" >> "baddns/signatures/signature_history.txt"
124-
125123
# Copy the file to the signatures directory
126124
cp "signatures_to_test/$file" "baddns/signatures/$file"
127125
@@ -167,13 +165,12 @@ jobs:
167165
echo "checking out new branch $BRANCH_NAME" >> readsources_action.log
168166
git checkout -b $BRANCH_NAME
169167
git add "baddns/signatures/$file"
170-
git add "baddns/signatures/signature_history.txt"
171168
echo "about to commit with the following files changed:" >> readsources_action.log
172169
git status -s >> readsources_action.log
173170
174171
echo "adding commit and pushing branch..." >> readsources_action.log
175172
# If the commit operation is successful, then push the changes and create a PR
176-
git commit -m "[SignatureBot] Add or update signature $file and update signature history" && {
173+
git commit -m "[SignatureBot] Add or update signature $file" && {
177174
git push origin new-signature-$file
178175
echo "## Add or update signature: $file" > pr_message
179176
echo "This PR adds or updates the follow signature:" >> pr_message
@@ -183,13 +180,14 @@ jobs:
183180
echo '```' >> pr_message
184181
echo "creating PR: '[SignatureBot] Add or update signature $file'" >> readsources_action.log
185182
sleep 5
186-
gh pr create --title "[SignatureBot] Add or update signature $file" --body-file pr_message --head new-signature-$file --repo ${{ github.repository }} 2>&1 | tee -a readsources_action.log
183+
gh pr create --base dev --title "[SignatureBot] Add or update signature $file" --body-file pr_message --head new-signature-$file --repo ${{ github.repository }} 2>&1 | tee -a readsources_action.log
187184
}
188-
git checkout main
185+
git checkout dev
189186
else
190187
echo "Skipping signature file: $file - no changes detected" >> readsources_action.log
191188
fi
192189
done
190+
193191
echo "completed read sources" >> readsources_action.log
194192
195193
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Update Signature History
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
branches: [dev]
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
update-history:
13+
if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, '[SignatureBot]')
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
ref: dev
19+
20+
- name: Update signature history
21+
env:
22+
GH_TOKEN: ${{ github.token }}
23+
run: |
24+
git config --local user.email "action@github.com"
25+
git config --local user.name "GitHub Action"
26+
27+
# Get signature files changed in the merged PR
28+
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | grep '^baddns/signatures/.*\.yml$' || true)
29+
30+
if [[ -z "$CHANGED_FILES" ]]; then
31+
echo "No signature files changed in this PR"
32+
exit 0
33+
fi
34+
35+
for file in $CHANGED_FILES; do
36+
if [[ -f "$file" ]]; then
37+
filename=$(basename "$file")
38+
hash=$(sha256sum "$file" | cut -d ' ' -f 1)
39+
40+
if ! grep -q "$hash" baddns/signatures/signature_history.txt 2>/dev/null; then
41+
echo "$hash #$filename" >> baddns/signatures/signature_history.txt
42+
echo "Added hash for $filename"
43+
fi
44+
fi
45+
done
46+
47+
if git diff --quiet baddns/signatures/signature_history.txt 2>/dev/null; then
48+
echo "No new hashes to add"
49+
else
50+
git add baddns/signatures/signature_history.txt
51+
git commit -m "[SignatureBot] Update signature history"
52+
git push origin dev
53+
fi

.github/workflows/tests.yaml

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,32 @@ jobs:
1919
if: contains(github.event.pull_request.title, 'SignatureBot') == false
2020
runs-on: ubuntu-latest
2121
steps:
22-
- uses: actions/checkout@v3
23-
- uses: psf/black@stable
24-
with:
25-
options: "--check"
22+
- uses: actions/checkout@v4
2623
- name: Install Python 3
27-
uses: actions/setup-python@v4
24+
uses: actions/setup-python@v5
2825
with:
29-
python-version: 3.9
30-
- name: Install dependencies
31-
run: |
32-
pip install flake8
33-
- name: flake8
34-
run: |
35-
flake8 --select F,E722 --ignore F403,F405,F541 --per-file-ignores="*/__init__.py:F401,F403"
36-
26+
python-version: "3.10"
27+
- name: Install ruff
28+
run: pip install ruff
29+
- name: ruff format
30+
run: ruff format --check .
31+
- name: ruff check
32+
run: ruff check .
33+
3734
test:
3835
if: contains(github.event.pull_request.title, 'SignatureBot') == false
3936
runs-on: ubuntu-latest
37+
strategy:
38+
matrix:
39+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
40+
fail-fast: false
4041
steps:
41-
- uses: actions/checkout@v3
42-
- name: Set up Python
43-
uses: actions/setup-python@v4
42+
- uses: actions/checkout@v4
43+
- name: Set up Python ${{ matrix.python-version }}
44+
uses: actions/setup-python@v5
4445
with:
45-
python-version: "3.10"
46+
python-version: ${{ matrix.python-version }}
47+
allow-prereleases: true
4648
- name: Install dependencies
4749
run: |
4850
pip install poetry
@@ -67,13 +69,13 @@ jobs:
6769
needs: test
6870
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
6971
steps:
70-
- uses: actions/checkout@v3
72+
- uses: actions/checkout@v4
7173
with:
7274
fetch-depth: 0
7375
- name: Set up Python
74-
uses: actions/setup-python@v4
76+
uses: actions/setup-python@v5
7577
with:
76-
python-version: "3.10"
78+
python-version: "3.12"
7779
- name: Install dependencies
7880
run: |
7981
python -m pip install --upgrade pip
@@ -96,11 +98,11 @@ jobs:
9698
run: |
9799
CURRENT_VERSION="${{ env.VERSION }}"
98100
LATEST_VERSION="${{ env.LATEST_TAG }}"
99-
101+
100102
# Extract major.minor for comparison
101103
CURRENT_MAJOR_MINOR=$(echo "$CURRENT_VERSION" | cut -d '.' -f 1-2)
102104
LATEST_MAJOR_MINOR=$(echo "$LATEST_VERSION" | cut -d '.' -f 1-2)
103-
105+
104106
# Compare versions
105107
if [ "$CURRENT_MAJOR_MINOR" == "$LATEST_MAJOR_MINOR" ]; then
106108
echo "VERSION_CHANGE=false" >> $GITHUB_ENV

CLAUDE.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
BadDNS is a Python tool for detecting subdomain takeovers and DNS issues (dangling CNAME/NS/MX records, NSEC walks, zone transfers, HTML reference hijacking, DMARC misconfigurations, MTA-STS issues, wildcard DNS takeovers). It's also used as a BBOT module.
8+
9+
## Common Commands
10+
11+
### Install dependencies
12+
```bash
13+
pip install poetry
14+
poetry install
15+
```
16+
17+
### Run all tests
18+
```bash
19+
poetry run pytest --exitfirst --disable-warnings --log-cli-level=DEBUG
20+
```
21+
22+
### Run a single test file
23+
```bash
24+
poetry run pytest tests/cname_test.py -v
25+
```
26+
27+
### Run a single test
28+
```bash
29+
poetry run pytest tests/cname_test.py::test_cname_function_name -v
30+
```
31+
32+
### Lint
33+
```bash
34+
ruff format --check .
35+
ruff check .
36+
```
37+
38+
### Format code
39+
```bash
40+
ruff format .
41+
```
42+
43+
### Run the CLI
44+
```bash
45+
poetry run baddns example.com
46+
poetry run baddns -m CNAME,NS example.com # specific modules
47+
poetry run baddns -d example.com # debug mode
48+
poetry run baddns --direct example.com # direct mode (CNAME only)
49+
```
50+
51+
## Architecture
52+
53+
### Module System
54+
55+
All detection logic lives in `baddns/modules/`. Each module is a class inheriting from `BadDNS_base` (defined in `baddns/base.py`). Modules are auto-discovered and dynamically imported by `baddns/__init__.py` — just drop a new `.py` file in `modules/` and it's available.
56+
57+
The 10 modules: **CNAME** (dangling CNAMEs), **NS** (dangling nameservers), **MX** (dangling mail exchangers), **NSEC** (NSEC walking for subdomain enumeration), **TXT** (hijackable domains in TXT records), **references** (hijackable domains in HTML/headers), **zonetransfer** (AXFR vulnerability), **DMARC** (missing/misconfigured DMARC records), **MTA-STS** (MTA-STS misconfigurations and dangling mta-sts subdomains), **WILDCARD** (wildcard DNS records enabling domain-wide takeover).
58+
59+
### Signature-Driven Detection
60+
61+
Signatures are YAML files in `baddns/signatures/` (~100 files). Each signature defines a service name, detection mode (`http`, `dns_nxdomain`, `dns_nosoa`), identifier patterns (cnames, IPs, nameservers), and HTTP matcher rules. The `Signature` class (`baddns/lib/signature.py`) loads them, and `Matcher` (`baddns/lib/matcher.py`) evaluates HTTP responses against matcher rules.
62+
63+
### Core Libraries (`baddns/lib/`)
64+
65+
- **DNSManager** (`dnsmanager.py`) — async DNS resolution with retry, CNAME chain following, multi-record-type dispatch
66+
- **HttpManager** (`httpmanager.py`) — fires 4 async HTTP requests per target (http/https × follow/deny redirects)
67+
- **WhoisManager** (`whoismanager.py`) — async WHOIS lookups, checks domain registration/expiration
68+
- **DnsWalk** (`dnswalk.py`) — recursive nameserver tracing from root servers, used by NS module
69+
- **Finding** (`findings.py`) — structured output with confidence levels (CONFIRMED/PROBABLE/POSSIBLE/UNLIKELY)
70+
71+
### Execution Flow
72+
73+
CLI (`baddns/cli.py`) → validates args → loads signatures → instantiates selected modules → calls each module's async `dispatch()` → collects `Finding` objects → outputs JSON.
74+
75+
## Testing
76+
77+
Tests are in `tests/` and heavily mock DNS/HTTP/WHOIS. Key test infrastructure:
78+
79+
- `tests/conftest.py` — shared fixtures (`mock_dispatch_whois`, `cached_suffix_list`, `configure_mock_resolver`)
80+
- `tests/helpers.py``MockResolver`, `MockDNSWalk`, `DnsWalkHarness` for DNS mocking
81+
- Tests use `pytest-asyncio` for async, `pytest-httpx` for HTTP mocking, `pyfakefs` for filesystem mocking
82+
83+
## Versioning
84+
85+
Version is tracked in **two places** in `pyproject.toml`:
86+
- `tool.poetry.version` (e.g., `"2.0.0"`)
87+
- `tool.poetry-dynamic-versioning.format` (e.g., `'2.0.{distance}'`)
88+
89+
Both must be updated together for releases. Publishing to PyPI happens automatically on push to `main` when the major.minor version changes.
90+
91+
## Git Workflow
92+
93+
- `main` branch — stable releases, auto-publishes to PyPI
94+
- `dev` branch — active development, PRs target here
95+
- Do not add "Co-Authored-By" lines to commit messages

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# BadDNS
22
Check subdomains for subdomain takeovers and other DNS tomfoolery
33

4-
[![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
4+
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
55
![License](https://img.shields.io/badge/license-GPLv3-f126ea.svg)
66
[![tests](https://github.com/blacklanternsecurity/baddns/actions/workflows/tests.yaml/badge.svg)](https://github.com/blacklanternsecurity/baddns/actions/workflows/tests.yaml)
77
[![codecov](https://codecov.io/gh/blacklanternsecurity/baddns/branch/main/graph/badge.svg)](https://codecov.io/gh/blacklanternsecurity/baddns)
@@ -24,7 +24,7 @@ Or use pipx: `pipx install git+https://github.com/blacklanternsecurity/baddns`
2424
After installing with pip, you can just run `baddns` from the command line.
2525

2626
```
27-
usage: baddns [-h] [-n CUSTOM_NAMESERVERS] [-c CUSTOM_SIGNATURES] [-l] [-s] [-m MODULES] [-d] [target]
27+
usage: baddns [-h] [-n CUSTOM_NAMESERVERS] [-c CUSTOM_SIGNATURES] [-l] [-s] [-m MODULES] [-d] [-D] [target]
2828
2929
Check subdomains for subdomain takeovers and other DNS tomfoolery
3030
@@ -38,10 +38,11 @@ options:
3838
-c CUSTOM_SIGNATURES, --custom-signatures CUSTOM_SIGNATURES
3939
Use an alternate directory for loading signatures
4040
-l, --list-modules List available modules and their descriptions.
41-
-s, --silent Show only vulnerable targets
41+
-s, --silent Only show results, no other output (JSON format)
4242
-m MODULES, --modules MODULES
4343
Comma separated list of module names to use. Ex: module1,module2,module3
4444
-d, --debug Enable debug logging
45+
-D, --direct Enable direct mode
4546
4647
```
4748
## Modules
@@ -54,6 +55,10 @@ options:
5455
| references | Check HTML content for links or other references that contain a hijackable domain |
5556
| txt | Check TXT record contents for hijackable domains |
5657
| zonetransfer | Attempt a DNS zone transfer |
58+
| dmarc | Check for missing or misconfigured DMARC records |
59+
| mta-sts | Check for MTA-STS misconfigurations and dangling mta-sts subdomains |
60+
| wildcard | Check for wildcard DNS records that could enable domain-wide subdomain takeover |
61+
| spf | Check for missing or misconfigured SPF records and hijackable include/redirect domains |
5762

5863
## Examples
5964

baddns/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import logging
22

3+
from cloudcheck import CloudCheck
4+
35
log = logging.getLogger(__name__)
46

57

68
class BadDNS_base:
9+
skip_cloud_targets = False
10+
711
def __init__(
812
self,
913
target,
@@ -32,6 +36,18 @@ def infomsg(self, msg):
3236
else:
3337
log.debug(msg)
3438

39+
async def dispatch(self):
40+
if any(label.startswith("_") for label in self.target.split(".")):
41+
log.debug(f"Skipping SRV-style target [{self.target}], SRV-style subdomains are not supported")
42+
return False
43+
if self.skip_cloud_targets and await CloudCheck().lookup(self.target):
44+
log.debug(f"Skipping cloud provider target [{self.target}] for module [{self.__class__.__name__}]")
45+
return False
46+
return await self._dispatch()
47+
48+
async def _dispatch(self):
49+
raise NotImplementedError
50+
3551
async def cleanup(self):
3652
pass
3753

0 commit comments

Comments
 (0)