Building a Domain Security Analyzer — The Entry Point
In the last post, we built the CLI module that formats and displays our scan results. Now we have all the pieces:
Scanner package — Fetches security data
Models package — Wraps data with convenient properties
CLI module — Displays results beautifully
Today we build the glue — the entry point that ties everything together.
Today's Goal
By the end of this post, you'll understand:
What an entry point is — The first code that runs when users invoke your program
What the shebang line does — Making scripts executable on Unix systems
How
sys.argvworks — Reading command-line argumentsHow the data flows through our architecture — From user input to final output
The Complete Code
Here's the full analyzer.py file. It's intentionally short — all the heavy lifting happens in our packages:
#!/usr/bin/env python3
"""
Domain Security Analyzer
A tool to analyze domain security configurations including
DNS records, SSL certificates, and HTTP security headers.
Usage:
python analyzer.py <domain>
Example:
python analyzer.py github.com
"""
import sys
from scanner import Scanner
from models import ScanReport
import cli
def main():
"""Main entry point for the CLI."""
if len(sys.argv) != 2:
print("Usage: python analyzer.py <domain>")
print("Example: python analyzer.py github.com")
sys.exit(1)
domain = sys.argv[1]
# Run the scan
print(f"\nScanning {domain}...")
scanner = Scanner(domain)
results = scanner.scan()
# Create report from results
report = ScanReport(results)
# Display the report
cli.display_report(report)
if __name__ == "__main__":
main()
Now let's understand every piece.
Part 1: The Shebang Line
#!/usr/bin/env python3
This line is called a "shebang" (from "hash-bang" — the #! characters). It's not Python code — it's an instruction to Unix-based operating systems (Linux, macOS).
What it does:
When you run a script directly:
./analyzer.py github.com
The operating system reads the first line to figure out how to run the file. The shebang says: "Find python3 using the env command, then use it to run this script."
Why /usr/bin/env python3 instead of just /usr/bin/python3?
Python might be installed in different locations:
/usr/bin/python3 # System Python on Linux
/usr/local/bin/python3 # Homebrew Python on macOS
/home/user/.pyenv/... # pyenv-managed Python
The env command searches your PATH and finds Python wherever it's installed. This makes the script portable across different systems.
Windows note:
Windows ignores the shebang line. It uses file associations instead — .py files are associated with Python. But the line doesn't hurt, and it makes your code work on Linux/Mac too.
Part 2: The Module Docstring
"""
Domain Security Analyzer
A tool to analyze domain security configurations including
DNS records, SSL certificates, and HTTP security headers.
Usage:
python analyzer.py <domain>
Example:
python analyzer.py github.com
"""
This is a multi-line docstring at the top of the file. It serves multiple purposes:
Documentation — Anyone reading the file immediately knows what it does
Help text — Some tools extract this to show users (like
pydoc analyzer)Usage instructions — Shows how to run the program with examples
Why include usage in the docstring?
Users often open the main file to see how to use a tool. Having usage right at the top means they don't have to hunt for it.
Part 3: The Imports
import sys
from scanner import Scanner
from models import ScanReport
import cli
Let's break down each import and what it brings:
import sys
The sys module is Python's interface to the interpreter and operating system. We use it for:
sys.argv— List of command-line argumentssys.exit()— Exit the program with a status code
from scanner import Scanner
This imports the Scanner class from our scanner package. Remember how we set up scanner/__init__.py:
from .scanner import Scanner
__all__ = ['Scanner']
That __init__.py file makes this clean import possible. Without it, we'd need:
from scanner.scanner import Scanner # Ugly
from models import ScanReport
Same pattern. The models/__init__.py exposes ScanReport, so we can import it directly from the package.
import cli
This imports the entire cli module. We use cli.display_report() rather than importing the function directly. Either approach works:
# Option 1: Import the module
import cli
cli.display_report(report)
# Option 2: Import the function
from cli import display_report
display_report(report)
We use Option 1 because it makes it clear where display_report comes from when reading the code.
Part 4: Argument Validation
def main():
"""Main entry point for the CLI."""
if len(sys.argv) != 2:
print("Usage: python analyzer.py <domain>")
print("Example: python analyzer.py github.com")
sys.exit(1)
What is sys.argv?
When you run a program, everything you type becomes a list:
python analyzer.py github.com
Becomes:
sys.argv = ['analyzer.py', 'github.com']
# [0] [1]
The first element (sys.argv[0]) is always the script name. Arguments come after.
Why check len(sys.argv) != 2?
We expect exactly two items:
The script name (always present)
The domain to scan (user provides this)
If the length isn't 2, something's wrong:
python analyzer.py # len = 1, missing domain
python analyzer.py a b c # len = 4, too many arguments
What is sys.exit(1)?
Exit codes tell the operating system whether the program succeeded:
0= SuccessNon-zero = Failure (by convention,
1for general errors)
This matters for scripts and automation:
python analyzer.py github.com && echo "Success!"
The && only runs "Success!" if the first command exits with 0.
Part 5: Getting the Domain
domain = sys.argv[1]
Simple — extract the domain from the argument list. After validation, we know sys.argv[1] exists.
No validation here?
We don't check if it's a valid domain format. The Scanner will handle that — if DNS lookup fails, we'll get an error in the results. This follows a principle: validate at the boundary where the data is used, not where it enters.
Part 6: Creating the Scanner
# Run the scan
print(f"\nScanning {domain}...")
scanner = Scanner(domain)
results = scanner.scan()
The progress message:
print(f"\nScanning {domain}...")
This tells users something is happening. Security scans take a few seconds (DNS lookups, SSL handshakes, HTTP requests). Without feedback, users might think the program froze.
The \n adds a blank line before the message for visual separation.
Creating the Scanner:
scanner = Scanner(domain)
This creates a Scanner instance. Remember from our Scanner class:
def __init__(self, domain):
self.domain = domain.lower().strip()
self.results = None
self.scan_time = None
The constructor stores the domain (normalized to lowercase, whitespace removed) but doesn't do any scanning yet.
Running the scan:
results = scanner.scan()
This calls the scan() method, which:
Records the current time
Runs DNS checks via
dns_scanner.scan_dns()Runs SSL checks via
ssl_scanner.scan_ssl()Runs header checks via
headers_scanner.scan_headers()Returns a dictionary with all results
The results variable now holds a nested dictionary like:
{
'domain': 'github.com',
'scan_time': '2024-12-25T16:15:00+00:00',
'dns': { ... },
'ssl': { ... },
'headers': { ... }
}
Part 7: Creating the Report
# Create report from results
report = ScanReport(results)
The raw results dictionary works, but it's tedious to use:
# Without ScanReport
if results['ssl'].get('success') and results['ssl'].get('valid'):
days = results['ssl']['days_until_expiry']
ScanReport wraps the dictionary and provides convenient properties:
# With ScanReport
if report.has_valid_ssl:
days = report.ssl_days_remaining
The report doesn't duplicate data — it stores a reference to the results dictionary and provides cleaner access to it.
Part 8: Displaying the Report
# Display the report
cli.display_report(report)
This calls our CLI module's main display function. It:
Prints the header with domain and timestamp
Calls
display_dns(report)for DNS sectionCalls
display_ssl(report)for SSL sectionCalls
display_headers(report)for headers section
Why pass the report, not results?
The cli module uses properties like report.has_valid_ssl and report.missing_headers. These come from ScanReport, not the raw dictionary. The report provides the interface that display functions expect.
Part 9: The Entry Point Guard
if __name__ == "__main__":
main()
We covered this in Post 1, but let's revisit it in context.
The problem it solves:
When Python runs a file directly:
python analyzer.py github.com
It sets __name__ = "__main__", and main() runs.
When Python imports the file:
# In some test file
import analyzer
# We just want access to the code, not to run a scan
It sets __name__ = "analyzer", and main() does NOT run.
Why have a main() function at all?
We could put all the code directly under if __name__ == "__main__":, but a function is better:
Testability — We can call
main()from testsClarity — The function name describes what happens
Scope — Variables inside
main()don't pollute the module namespace
Part 10: The Complete Data Flow
Let's trace what happens when a user runs our tool:
User types: python analyzer.py github.com
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. Python loads analyzer.py │
│ - Imports sys, Scanner, ScanReport, cli │
│ - __name__ == "__main__", so main() runs │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. main() validates arguments │
│ - sys.argv = ['analyzer.py', 'github.com'] │
│ - len(sys.argv) == 2, validation passes │
│ - domain = 'github.com' │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Scanner performs checks │
│ - dns_scanner.scan_dns() → DNS records │
│ - ssl_scanner.scan_ssl() → Certificate info │
│ - headers_scanner.scan_headers() → Security headers │
│ - Returns combined dictionary │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. ScanReport wraps results │
│ - Provides properties: has_valid_ssl, missing_headers, etc. │
│ - Makes data easy to work with │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. cli.display_report() shows output │
│ - Formats and prints each section │
│ - User sees the security report │
└─────────────────────────────────────────────────────────────────┘
This flow demonstrates separation of concerns:
analyzer.py— Orchestrates the processScanner— Gathers dataScanReport— Provides data interfacecli— Displays data
Each piece has one job. If we want a web interface, we replace step 5 with HTML templates. The rest stays the same.
Part 11: Final Project Structure
Here's our complete project after this post:
src/
├── scanner/
│ ├── __init__.py # Exposes Scanner class
│ ├── scanner.py # Main Scanner class
│ ├── dns_scanner.py # DNS record checks
│ ├── ssl_scanner.py # SSL certificate checks
│ └── headers_scanner.py # HTTP header checks
├── models/
│ ├── __init__.py # Exposes ScanReport
│ └── report.py # ScanReport class
├── cli.py # Terminal display functions
├── analyzer.py # Entry point (this post)
├── requirements.txt # Dependencies
└── venv/ # Virtual environment
Layer responsibilities:
| Layer | Files | Purpose |
| Entry | analyzer.py | Parse arguments, orchestrate flow |
| Display | cli.py | Format and print output |
| Model | models/report.py | Data access interface |
| Scanner | scanner/*.py | Perform security checks |
Part 12: Running the Complete Tool
From the src folder:
python analyzer.py github.com
Output:
Scanning github.com...
============================================================
Security Report for github.com
Scanned at: 2024-12-25T16:15:00.501688+00:00
============================================================
DNS Records
----------------------------------------
A Records (IPv4):
→ 20.207.73.82
MX Records (Mail Servers):
→ [1] aspmx.l.google.com
→ [5] alt1.aspmx.l.google.com
→ [5] alt2.aspmx.l.google.com
→ [10] alt3.aspmx.l.google.com
→ [10] alt4.aspmx.l.google.com
SPF Record:
✓ v=spf1 ip4:192.30.252.0/22 include:_netblocks.google.com inc...
DMARC Record:
✓ v=DMARC1; p=reject; pct=100; rua=mailto:dmarc@github.com
SSL Certificate
----------------------------------------
✓ Valid Certificate
Issued to: github.com
Issued by: Sectigo Limited
Valid from: 2025-02-05
Expires: 2026-02-05 (42 days)
→ Also valid for: github.com, www.github.com
Security Headers
----------------------------------------
URL: https://github.com/
Status: 200
✓ Strict-Transport-Security
max-age=31536000; includeSubdomains; preload
✓ Content-Security-Policy
default-src 'none'; base-uri 'self'; child-sr...
✓ X-Frame-Options
deny
✓ X-Content-Type-Options
nosniff
✓ Referrer-Policy
origin-when-cross-origin, strict-origin-when-cross...
✗ Permissions-Policy - MISSING
(Controls browser feature access)
Try other domains:
python analyzer.py example.com # Simple site, fewer protections
python analyzer.py facebook.com # Different security config
python analyzer.py expired.badssl.com # Intentionally broken SSL
Summary: What We've Learned
The shebang line (
#!/usr/bin/env python3) — Tells Unix systems how to run the script directlysys.argvholds command-line arguments —sys.argv[0]is the script,sys.argv[1:]are user argumentssys.exit(code)signals success or failure —0for success, non-zero for errorsThe entry point orchestrates, doesn't implement — It connects pieces together without duplicating their logic
Separation of concerns enables flexibility — Each layer has one job; replacing any layer doesn't affect others
The
if __name__ == "__main__"guard — Preventsmain()from running when the file is imported
What's Next
We now have a complete, working domain security analyzer:
✓ DNS record analysis
✓ SSL certificate validation
✓ HTTP security headers
✓ Clean architecture
✓ Beautiful terminal output
But users still have to interpret the results themselves. Is this domain secure? What should they fix first?
In the next post, we'll build the Scoring Engine — turning all this data into:
Letter grades (A+ to F) — Overall and per-category
Critical issues vs warnings — Priority ranking
Specific recommendations — What to fix and why
The raw data becomes actionable insight.
Commit Your Progress
git add .
git commit -m "Add entry point and complete CLI tool"
git push origin main
See you in the next one.
This is Part 13 of the Domain Security Analyzer series.
Find the code on GitHub.