mirror of
https://github.com/saymrwulf/CertTransparencySearch.git
synced 2026-05-14 20:37:52 +00:00
Consolidate monograph and refine lineage red flags
This commit is contained in:
parent
b4b16bd2d3
commit
d77730390a
5 changed files with 1242 additions and 295 deletions
7
Makefile
7
Makefile
|
|
@ -60,9 +60,6 @@ monograph:
|
|||
--max-candidates-per-domain $(MAX_CANDIDATES) \
|
||||
--markdown-output output/corpus/monograph.md \
|
||||
--latex-output output/corpus/monograph.tex \
|
||||
--pdf-output output/corpus/monograph.pdf \
|
||||
--appendix-markdown-output output/corpus/appendix-inventory.md \
|
||||
--appendix-latex-output output/corpus/appendix-inventory.tex \
|
||||
--appendix-pdf-output output/corpus/appendix-inventory.pdf
|
||||
--pdf-output output/corpus/monograph.pdf
|
||||
|
||||
all: init-config purpose lineage monograph
|
||||
all: init-config monograph
|
||||
|
|
|
|||
26
README.md
26
README.md
|
|
@ -1,12 +1,12 @@
|
|||
# Certificate Transparency Search
|
||||
|
||||
This project builds a publication-grade report set from Certificate Transparency and public DNS:
|
||||
This project builds a publication-grade monograph from Certificate Transparency and public DNS:
|
||||
|
||||
- it finds currently valid leaf certificates whose SAN values contain configured search terms
|
||||
- it verifies locally that the certificates are real leaf certificates rather than CA certificates or precertificates
|
||||
- it assesses intended usage from EKU and KeyUsage
|
||||
- it scans the DNS names exposed by the SAN corpus
|
||||
- it produces readable Markdown, LaTeX, and PDF outputs
|
||||
- it produces one primary readable output set: a monograph in Markdown, LaTeX, and PDF
|
||||
|
||||
The project is designed for public source control:
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ Rules:
|
|||
|
||||
### Main publication
|
||||
|
||||
This is the publication-grade monograph with appendices:
|
||||
This is the single canonical publication. The appendices are embedded into the same monograph, so you do not need to manage separate visible appendix artefacts:
|
||||
|
||||
```bash
|
||||
make monograph
|
||||
|
|
@ -89,12 +89,13 @@ Outputs:
|
|||
- `output/corpus/monograph.md`
|
||||
- `output/corpus/monograph.tex`
|
||||
- `output/corpus/monograph.pdf`
|
||||
- `output/corpus/appendix-inventory.md`
|
||||
- `output/corpus/appendix-inventory.tex`
|
||||
- `output/corpus/appendix-inventory.pdf`
|
||||
|
||||
Internal helper artefacts used during PDF assembly are written only under `.cache/monograph-temp/`.
|
||||
|
||||
### Supporting purpose assessment
|
||||
|
||||
This is optional. Its findings are already woven into the monograph, but the standalone output can still be useful during development:
|
||||
|
||||
```bash
|
||||
make purpose
|
||||
```
|
||||
|
|
@ -106,6 +107,8 @@ Outputs:
|
|||
|
||||
### Historical lineage analysis
|
||||
|
||||
This is optional. Its findings are already woven into the monograph, but the standalone output can still be useful during development:
|
||||
|
||||
This report extends the analysis across current and expired certificates to study:
|
||||
|
||||
- repeated issuance under the same Subject CN
|
||||
|
|
@ -138,7 +141,7 @@ Outputs:
|
|||
|
||||
### Full operator run
|
||||
|
||||
This creates the local config if missing, then runs the purpose assessment, historical lineage analysis, and the full monograph:
|
||||
This creates the local config if missing, then builds the full monograph:
|
||||
|
||||
```bash
|
||||
make all
|
||||
|
|
@ -177,6 +180,8 @@ If you do not want to use `make`, the equivalent commands are:
|
|||
|
||||
### Inventory appendix source
|
||||
|
||||
This is only needed if you want the raw family inventory outside the monograph:
|
||||
|
||||
```bash
|
||||
.venv/bin/python ct_scan.py \
|
||||
--domains-file domains.local.txt \
|
||||
|
|
@ -233,10 +238,7 @@ If you do not want to use `make`, the equivalent commands are:
|
|||
--max-candidates-per-domain 10000 \
|
||||
--markdown-output output/corpus/monograph.md \
|
||||
--latex-output output/corpus/monograph.tex \
|
||||
--pdf-output output/corpus/monograph.pdf \
|
||||
--appendix-markdown-output output/corpus/appendix-inventory.md \
|
||||
--appendix-latex-output output/corpus/appendix-inventory.tex \
|
||||
--appendix-pdf-output output/corpus/appendix-inventory.pdf
|
||||
--pdf-output output/corpus/monograph.pdf
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
|
@ -246,7 +248,7 @@ If you do not want to use `make`, the equivalent commands are:
|
|||
- `ct_lineage_report.py`: historical Subject CN, Subject DN, issuer, SAN, and issuance-burst analysis
|
||||
- `ct_dns_utils.py`: DNS scanning and provider-signature logic
|
||||
- `ct_master_report.py`: shorter consolidated report
|
||||
- `ct_monograph_report.py`: publication-grade monograph with appendices
|
||||
- `ct_monograph_report.py`: publication-grade monograph with embedded appendices
|
||||
- `Makefile`: reproducible operator workflow
|
||||
|
||||
## Safety Against Silent Undercounts
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -131,10 +131,8 @@ def short_issuer_family(issuer_name: str) -> str:
|
|||
lowered = issuer_name.lower()
|
||||
if "amazon" in lowered:
|
||||
return "Amazon"
|
||||
if "sectigo" in lowered:
|
||||
return "Sectigo"
|
||||
if "comodo" in lowered:
|
||||
return "COMODO"
|
||||
if "sectigo" in lowered or "comodo" in lowered:
|
||||
return "Sectigo/COMODO"
|
||||
if "google trust services" in lowered or "cn=we1" in lowered:
|
||||
return "Google Trust Services"
|
||||
return "Other"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from collections import Counter
|
|||
from pathlib import Path
|
||||
|
||||
import ct_dns_utils
|
||||
import ct_lineage_report
|
||||
import ct_master_report
|
||||
import ct_scan
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ def parse_args() -> argparse.Namespace:
|
|||
parser.add_argument("--domains-file", type=Path, default=Path("domains.local.txt"))
|
||||
parser.add_argument("--cache-dir", type=Path, default=Path(".cache/ct-search"))
|
||||
parser.add_argument("--dns-cache-dir", type=Path, default=Path(".cache/dns-scan"))
|
||||
parser.add_argument("--history-cache-dir", type=Path, default=Path(".cache/ct-history-v2"))
|
||||
parser.add_argument("--cache-ttl-seconds", type=int, default=0)
|
||||
parser.add_argument("--dns-cache-ttl-seconds", type=int, default=86400)
|
||||
parser.add_argument("--max-candidates-per-domain", type=int, default=10000)
|
||||
|
|
@ -25,9 +27,9 @@ def parse_args() -> argparse.Namespace:
|
|||
parser.add_argument("--markdown-output", type=Path, default=Path("output/corpus/monograph.md"))
|
||||
parser.add_argument("--latex-output", type=Path, default=Path("output/corpus/monograph.tex"))
|
||||
parser.add_argument("--pdf-output", type=Path, default=Path("output/corpus/monograph.pdf"))
|
||||
parser.add_argument("--appendix-markdown-output", type=Path, default=Path("output/corpus/appendix-inventory.md"))
|
||||
parser.add_argument("--appendix-latex-output", type=Path, default=Path("output/corpus/appendix-inventory.tex"))
|
||||
parser.add_argument("--appendix-pdf-output", type=Path, default=Path("output/corpus/appendix-inventory.pdf"))
|
||||
parser.add_argument("--appendix-markdown-output", type=Path, default=Path(".cache/monograph-temp/appendix-inventory.md"))
|
||||
parser.add_argument("--appendix-latex-output", type=Path, default=Path(".cache/monograph-temp/appendix-inventory.tex"))
|
||||
parser.add_argument("--appendix-pdf-output", type=Path, default=Path(".cache/monograph-temp/appendix-inventory.pdf"))
|
||||
parser.add_argument("--skip-pdf", action="store_true")
|
||||
parser.add_argument("--pdf-engine", default="xelatex")
|
||||
parser.add_argument("--quiet", action="store_true")
|
||||
|
|
@ -88,10 +90,8 @@ def short_issuer(issuer_name: str) -> str:
|
|||
lowered = issuer_name.lower()
|
||||
if "amazon" in lowered:
|
||||
return "Amazon"
|
||||
if "sectigo" in lowered:
|
||||
return "Sectigo"
|
||||
if "comodo" in lowered:
|
||||
return "COMODO"
|
||||
if "sectigo" in lowered or "comodo" in lowered:
|
||||
return "Sectigo/COMODO"
|
||||
if "google trust services" in lowered or "cn=we1" in lowered:
|
||||
return "Google Trust Services"
|
||||
return issuer_name
|
||||
|
|
@ -173,7 +173,31 @@ def build_issuer_family_rows(report: dict[str, object]) -> list[dict[str, str]]:
|
|||
return result
|
||||
|
||||
|
||||
def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None:
|
||||
def build_history_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
domains_file=args.domains_file,
|
||||
cache_dir=args.history_cache_dir,
|
||||
cache_ttl_seconds=args.cache_ttl_seconds,
|
||||
max_candidates_per_domain=args.max_candidates_per_domain,
|
||||
retries=args.retries,
|
||||
quiet=args.quiet,
|
||||
markdown_output=Path(".cache/monograph-temp/unused-history.md"),
|
||||
latex_output=Path(".cache/monograph-temp/unused-history.tex"),
|
||||
pdf_output=Path(".cache/monograph-temp/unused-history.pdf"),
|
||||
skip_pdf=True,
|
||||
pdf_engine=args.pdf_engine,
|
||||
)
|
||||
|
||||
|
||||
def historical_repeated_cn_count(assessment: ct_lineage_report.HistoricalAssessment) -> int:
|
||||
return sum(1 for values in assessment.cn_groups.values() if len(values) > 1)
|
||||
|
||||
|
||||
def render_markdown(
|
||||
args: argparse.Namespace,
|
||||
report: dict[str, object],
|
||||
assessment: ct_lineage_report.HistoricalAssessment,
|
||||
) -> None:
|
||||
args.markdown_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
appendix_markdown = args.appendix_markdown_output.read_text(encoding="utf-8")
|
||||
hits = report["hits"]
|
||||
|
|
@ -187,6 +211,9 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
server_only_issuer_families = collapse_issuer_counts_by_family(
|
||||
purpose_summary.issuer_breakdown.get("tls_server_only", {})
|
||||
)
|
||||
historical_count = len(assessment.certificates)
|
||||
historical_current_count = sum(1 for item in assessment.certificates if item.current)
|
||||
repeated_cn_count = historical_repeated_cn_count(assessment)
|
||||
purpose_rows = [
|
||||
[
|
||||
purpose_label(category),
|
||||
|
|
@ -260,6 +287,7 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
f"- **{len(hits)}** current leaf certificates are in scope on this run.",
|
||||
f"- **{len(groups)}** CN families reduce the estate into readable naming clusters.",
|
||||
f"- **{purpose_summary.category_counts.get('tls_server_only', 0)}** certificates are strict TLS server certificates and **{purpose_summary.category_counts.get('tls_server_and_client', 0)}** are dual-EKU server-plus-client certificates.",
|
||||
f"- **{historical_count}** historical leaf certificates show how these names evolved over time, including expired issuance lineages.",
|
||||
f"- **{len(report['unique_dns_names'])}** unique DNS SAN names were scanned live.",
|
||||
"- The estate is best understood as several layers laid on top of one another: brand naming, service naming, platform naming, delivery-stack naming, and migration residue.",
|
||||
]
|
||||
|
|
@ -270,9 +298,10 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
lines.extend(
|
||||
[
|
||||
"- Read Chapter 1 if you want to know whether the corpus is complete and trustworthy.",
|
||||
"- Read Chapters 2 and 3 if you want the certificate-side story: issuers, trust, and purpose.",
|
||||
"- Read Chapters 4 and 5 if you want the naming and DNS story.",
|
||||
"- Read Chapter 6 if you want the synthesis that ties business naming, service architecture, and hosting patterns together.",
|
||||
"- Read Chapters 2 and 3 if you want the current certificate-side story: issuers, trust, and purpose.",
|
||||
"- Read Chapter 4 if you want the historical lifecycle view and the red flags split into current versus fixed-in-the-past.",
|
||||
"- Read Chapters 5 and 6 if you want the naming and DNS story.",
|
||||
"- Read Chapter 7 if you want the synthesis that ties business naming, service architecture, and hosting patterns together.",
|
||||
"- Use the appendices when you need the fine-grained evidence rather than the argument.",
|
||||
]
|
||||
)
|
||||
|
|
@ -404,7 +433,93 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
]
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## Chapter 4: Naming Architecture")
|
||||
lines.append("## Chapter 4: Historical Renewal, Drift, and Red Flags")
|
||||
lines.append("")
|
||||
lines.append("**Management Summary**")
|
||||
lines.append("")
|
||||
lines.extend(
|
||||
[
|
||||
f"- Historical leaf certificates in scope: {historical_count}, of which {historical_current_count} are still currently valid.",
|
||||
f"- Subject CN values with more than one certificate over time: {repeated_cn_count}.",
|
||||
f"- Renewal asset lineages with normal rollover overlap below 50 days: {assessment.normal_reissuance_assets}.",
|
||||
f"- Current overlap red flags at 50 days or more: {len(assessment.overlap_current_rows)}.",
|
||||
f"- Past-only overlap red flags now fixed: {len(assessment.overlap_past_rows)}.",
|
||||
f"- Current Subject DN drift / CA lineage drift / SAN drift counts: {len(assessment.dn_current_rows)} / {len(assessment.vendor_current_rows)} / {len(assessment.san_current_rows)}.",
|
||||
f"- Past-only Subject DN drift / CA lineage drift / SAN drift counts: {len(assessment.dn_past_rows)} / {len(assessment.vendor_past_rows)} / {len(assessment.san_past_rows)}.",
|
||||
]
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("This chapter is the historical control layer for the whole publication. It answers a different question from the current-corpus chapters above: not just what certificates exist now, but how the hostname estate has behaved over time.")
|
||||
lines.append("")
|
||||
lines.append("A normal renewal reissues what is essentially the same certificate asset with a new key and a new validity span, and predecessor and successor overlap only briefly. In this monograph, anything below 50 days of overlap is treated as normal. Fifty days or more is treated as a red flag. COMODO and Sectigo are treated as one CA lineage from the outset, so movement between those names is not counted here as lineage drift.")
|
||||
lines.append("")
|
||||
lines.append("### Current Red-Flag Inventory")
|
||||
lines.append("")
|
||||
if assessment.current_red_flag_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Score", "Certs", "Current", "Flags", "Issuer Mix"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.score),
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
row.flags,
|
||||
row.notes,
|
||||
]
|
||||
for row in assessment.current_red_flag_rows[:25]
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No current red flags were found under the configured rules.")
|
||||
lines.append("")
|
||||
lines.append("### Past Red Flags Now Fixed")
|
||||
lines.append("")
|
||||
if assessment.past_red_flag_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Score", "Certs", "Current", "Flags", "Issuer Mix"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.score),
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
row.flags,
|
||||
row.notes,
|
||||
]
|
||||
for row in assessment.past_red_flag_rows[:25]
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No past-only red flags were found under the configured rules.")
|
||||
lines.append("")
|
||||
lines.append("### What The Historical Red Flags Mean")
|
||||
lines.append("")
|
||||
lines.extend(
|
||||
[
|
||||
f"- **Overlap red flag**: a predecessor and successor inside the same renewal asset lineage coexist for 50 days or more. Current cases: {len(assessment.overlap_current_rows)}. Past-only fixed cases: {len(assessment.overlap_past_rows)}.",
|
||||
f"- **Subject DN drift**: the same Subject CN appears under more than one full Subject DN. Current cases: {len(assessment.dn_current_rows)}. Past-only fixed cases: {len(assessment.dn_past_rows)}.",
|
||||
f"- **CA lineage drift**: the same Subject CN appears under more than one CA lineage, after collapsing COMODO and Sectigo together. Current cases: {len(assessment.vendor_current_rows)}. Past-only fixed cases: {len(assessment.vendor_past_rows)}.",
|
||||
f"- **SAN drift**: the same Subject CN appears with more than one SAN profile. Current cases: {len(assessment.san_current_rows)}. Past-only fixed cases: {len(assessment.san_past_rows)}.",
|
||||
f"- **Exact issuer changes** inside one lineage also exist: {len(assessment.issuer_rows)} Subject CN values. Those are tracked as context, not as first-order lineage red flags.",
|
||||
]
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("### Historical Step Changes")
|
||||
lines.append("")
|
||||
lines.extend(
|
||||
[
|
||||
f"- Top issuance start dates: {', '.join(f'{row.start_day} ({row.certificate_count})' for row in assessment.day_rows[:6])}.",
|
||||
f"- Strong step weeks: {', '.join(f'{row.week_start} ({row.certificate_count} vs prior avg {row.prior_eight_week_avg})' for row in assessment.week_rows[:4]) or 'none'}.",
|
||||
"- These bursts matter because they show where certificate behaviour was driven by platform-scale operations rather than one-off manual issuance.",
|
||||
]
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## Chapter 5: Naming Architecture")
|
||||
lines.append("")
|
||||
lines.append("**Management Summary**")
|
||||
lines.append("")
|
||||
|
|
@ -429,7 +544,7 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
for point in example.evidence:
|
||||
lines.append(f"- Evidence: {point}")
|
||||
lines.append("")
|
||||
lines.append("## Chapter 5: DNS Delivery Architecture")
|
||||
lines.append("## Chapter 6: DNS Delivery Architecture")
|
||||
lines.append("")
|
||||
lines.append("**Management Summary**")
|
||||
lines.append("")
|
||||
|
|
@ -455,7 +570,7 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
lines.append("")
|
||||
lines.append("The important thing is not the vendor name by itself. The important thing is what role it implies. CloudFront implies a distribution edge. Apigee implies managed API exposure. Adobe Campaign implies a marketing or communications front. A load balancer implies traffic distribution to backend services.")
|
||||
lines.append("")
|
||||
lines.append("## Chapter 6: Making The Whole Estate Make Sense")
|
||||
lines.append("## Chapter 7: Making The Whole Estate Make Sense")
|
||||
lines.append("")
|
||||
lines.append("**Management Summary**")
|
||||
lines.append("")
|
||||
|
|
@ -472,7 +587,7 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
lines.append("")
|
||||
lines.append("This is why the estate can look both tidy and messy at once. It is tidy within each layer, but messy across layers because the layers are solving different problems.")
|
||||
lines.append("")
|
||||
lines.append("## Chapter 7: Limits, Confidence, and Noise")
|
||||
lines.append("## Chapter 8: Limits, Confidence, and Noise")
|
||||
lines.append("")
|
||||
lines.append("**Management Summary**")
|
||||
lines.append("")
|
||||
|
|
@ -491,13 +606,268 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
lines.append("")
|
||||
lines.extend(md_table(["ID", "Basis", "Type", "Certs", "CNs", "Top Stacks"], family_rows))
|
||||
lines.append("")
|
||||
if dual_rows:
|
||||
lines.append("## Appendix B: Detailed Dual-EKU Catalogue")
|
||||
lines.append("")
|
||||
lines.append("This appendix keeps the complete dual-EKU evidence available without letting the minority case dominate the main analytical chapter.")
|
||||
lines.append("")
|
||||
lines.extend(md_table(["Subject CN", "Valid From", "Valid To", "Issuer", "DNS SANs"], dual_rows))
|
||||
lines.append("")
|
||||
lines.append("## Appendix B: Historical Red-Flag Detail")
|
||||
lines.append("")
|
||||
lines.append("This appendix keeps the detailed historical evidence inside the monograph so that the reader does not need a second report.")
|
||||
lines.append("")
|
||||
lines.append("### B.1 Current Red-Flag Inventory")
|
||||
lines.append("")
|
||||
if assessment.current_red_flag_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Score", "Certs", "Current", "Flags", "Issuer Mix"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.score),
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
row.flags,
|
||||
row.notes,
|
||||
]
|
||||
for row in assessment.current_red_flag_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No current red flags were found.")
|
||||
lines.append("")
|
||||
lines.append("### B.2 Past Red-Flag Inventory Now Fixed")
|
||||
lines.append("")
|
||||
if assessment.past_red_flag_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Score", "Certs", "Current", "Flags", "Issuer Mix"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.score),
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
row.flags,
|
||||
row.notes,
|
||||
]
|
||||
for row in assessment.past_red_flag_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No past-only red flags were found.")
|
||||
lines.append("")
|
||||
lines.append("### B.3 Current Overlap Red Flags")
|
||||
lines.append("")
|
||||
if assessment.overlap_current_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Lineage", "Asset Certs", "Current", "Max Concurrent", "Max Overlap Days", "Class", "Asset Details"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
row.lineage,
|
||||
str(row.asset_variant_count),
|
||||
str(row.current_certificate_count),
|
||||
str(row.max_concurrent),
|
||||
str(row.max_overlap_days),
|
||||
row.overlap_class,
|
||||
row.details,
|
||||
]
|
||||
for row in assessment.overlap_current_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No current overlap red flags were found.")
|
||||
lines.append("")
|
||||
lines.append("### B.4 Past Overlap Red Flags Now Fixed")
|
||||
lines.append("")
|
||||
if assessment.overlap_past_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Lineage", "Asset Certs", "Current", "Max Concurrent", "Max Overlap Days", "Class", "Asset Details"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
row.lineage,
|
||||
str(row.asset_variant_count),
|
||||
str(row.current_certificate_count),
|
||||
str(row.max_concurrent),
|
||||
str(row.max_overlap_days),
|
||||
row.overlap_class,
|
||||
row.details,
|
||||
]
|
||||
for row in assessment.overlap_past_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No past overlap red flags were found.")
|
||||
lines.append("")
|
||||
lines.append("### B.5 Current Subject DN Drift")
|
||||
lines.append("")
|
||||
if assessment.dn_current_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Certs", "Current", "Distinct Subject DNs", "Issuer Families", "Subject DN Samples"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
str(row.distinct_value_count),
|
||||
row.issuer_families,
|
||||
row.details,
|
||||
]
|
||||
for row in assessment.dn_current_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No current Subject DN drift was found.")
|
||||
lines.append("")
|
||||
lines.append("### B.6 Past Subject DN Drift Now Fixed")
|
||||
lines.append("")
|
||||
if assessment.dn_past_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Certs", "Current", "Distinct Subject DNs", "Issuer Families", "Subject DN Samples"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
str(row.distinct_value_count),
|
||||
row.issuer_families,
|
||||
row.details,
|
||||
]
|
||||
for row in assessment.dn_past_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No past-only Subject DN drift was found.")
|
||||
lines.append("")
|
||||
lines.append("### B.7 Current CA Lineage Drift")
|
||||
lines.append("")
|
||||
if assessment.vendor_current_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Certs", "Current", "Distinct Lineages", "Lineage Mix", "Lineages Seen"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
str(row.distinct_value_count),
|
||||
row.issuer_families,
|
||||
row.details,
|
||||
]
|
||||
for row in assessment.vendor_current_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No current CA lineage drift was found.")
|
||||
lines.append("")
|
||||
lines.append("### B.8 Past CA Lineage Drift Now Fixed")
|
||||
lines.append("")
|
||||
if assessment.vendor_past_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Certs", "Current", "Distinct Lineages", "Lineage Mix", "Lineages Seen"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
str(row.distinct_value_count),
|
||||
row.issuer_families,
|
||||
row.details,
|
||||
]
|
||||
for row in assessment.vendor_past_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No past-only CA lineage drift was found.")
|
||||
lines.append("")
|
||||
lines.append("### B.9 Current SAN Drift")
|
||||
lines.append("")
|
||||
if assessment.san_current_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Certs", "Current", "SAN Profiles", "Stable SANs", "Variable SANs", "Delta Pattern", "Representative Delta"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
str(row.distinct_san_profiles),
|
||||
str(row.stable_entries),
|
||||
str(row.variable_entries),
|
||||
row.delta_pattern,
|
||||
row.representative_delta,
|
||||
]
|
||||
for row in assessment.san_current_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No current SAN drift was found.")
|
||||
lines.append("")
|
||||
lines.append("### B.10 Past SAN Drift Now Fixed")
|
||||
lines.append("")
|
||||
if assessment.san_past_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Subject CN", "Certs", "Current", "SAN Profiles", "Stable SANs", "Variable SANs", "Delta Pattern", "Representative Delta"],
|
||||
[
|
||||
[
|
||||
row.subject_cn,
|
||||
str(row.certificate_count),
|
||||
str(row.current_certificate_count),
|
||||
str(row.distinct_san_profiles),
|
||||
str(row.stable_entries),
|
||||
str(row.variable_entries),
|
||||
row.delta_pattern,
|
||||
row.representative_delta,
|
||||
]
|
||||
for row in assessment.san_past_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No past-only SAN drift was found.")
|
||||
lines.append("")
|
||||
lines.append("### B.11 Historic Start Dates")
|
||||
lines.append("")
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Start Day", "Certificates", "Top Subject CNs", "Top Issuer Families"],
|
||||
[[row.start_day, str(row.certificate_count), row.top_subjects, row.top_issuers] for row in assessment.day_rows],
|
||||
)
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("### B.12 Historic Step Weeks")
|
||||
lines.append("")
|
||||
if assessment.week_rows:
|
||||
lines.extend(
|
||||
md_table(
|
||||
["Week Start", "Certificates", "Prior 8-Week Avg", "Top Subject CNs", "Top Issuer Families"],
|
||||
[
|
||||
[
|
||||
row.week_start,
|
||||
str(row.certificate_count),
|
||||
row.prior_eight_week_avg,
|
||||
row.top_subjects,
|
||||
row.top_issuers,
|
||||
]
|
||||
for row in assessment.week_rows
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append("No step weeks met the threshold.")
|
||||
lines.append("")
|
||||
lines.append("## Appendix C: Detailed Inventory Appendix")
|
||||
lines.append("")
|
||||
lines.append("The full issuer-first family inventory is reproduced below so that the monograph remains complete rather than merely interpretive.")
|
||||
|
|
@ -506,7 +876,11 @@ def render_markdown(args: argparse.Namespace, report: dict[str, object]) -> None
|
|||
args.markdown_output.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def render_latex(args: argparse.Namespace, report: dict[str, object]) -> None:
|
||||
def render_latex(
|
||||
args: argparse.Namespace,
|
||||
report: dict[str, object],
|
||||
assessment: ct_lineage_report.HistoricalAssessment,
|
||||
) -> None:
|
||||
args.latex_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
hits = report["hits"]
|
||||
groups = report["groups"]
|
||||
|
|
@ -521,6 +895,9 @@ def render_latex(args: argparse.Namespace, report: dict[str, object]) -> None:
|
|||
server_only_issuer_families = collapse_issuer_counts_by_family(
|
||||
purpose_summary.issuer_breakdown.get("tls_server_only", {})
|
||||
)
|
||||
historical_count = len(assessment.certificates)
|
||||
historical_current_count = sum(1 for item in assessment.certificates if item.current)
|
||||
repeated_cn_count = historical_repeated_cn_count(assessment)
|
||||
purpose_rows = [
|
||||
(
|
||||
purpose_label(category),
|
||||
|
|
@ -590,6 +967,7 @@ def render_latex(args: argparse.Namespace, report: dict[str, object]) -> None:
|
|||
r"\vspace{12pt}",
|
||||
r"\SummaryBox{"
|
||||
+ rf"\textbf{{Headline}}: {len(hits)} leaf certificates, {len(groups)} CN families, "
|
||||
+ rf"{historical_count} historical leaf certificates, "
|
||||
+ rf"{len(report['unique_dns_names'])} DNS names, "
|
||||
+ rf"{purpose_summary.category_counts.get('tls_server_only', 0)} strict server-auth certificates, "
|
||||
+ rf"{purpose_summary.category_counts.get('tls_server_and_client', 0)} dual-EKU certificates."
|
||||
|
|
@ -612,6 +990,7 @@ def render_latex(args: argparse.Namespace, report: dict[str, object]) -> None:
|
|||
f"{len(hits)} current leaf certificates are in scope on this run.",
|
||||
f"{len(groups)} CN families reduce the estate into readable naming clusters.",
|
||||
f"{purpose_summary.category_counts.get('tls_server_only', 0)} certificates are strict server-auth and {purpose_summary.category_counts.get('tls_server_and_client', 0)} are dual-EKU.",
|
||||
f"{historical_count} historical leaf certificates show how the same names evolved over time.",
|
||||
f"{len(report['unique_dns_names'])} DNS SAN names were scanned live.",
|
||||
"The estate is best understood as layers of branding, service naming, platform naming, and delivery naming rather than as random clutter.",
|
||||
]
|
||||
|
|
@ -625,10 +1004,11 @@ def render_latex(args: argparse.Namespace, report: dict[str, object]) -> None:
|
|||
add_summary(
|
||||
[
|
||||
"Chapter 1 proves the corpus and explains why the numbers can be trusted.",
|
||||
"Chapters 2 and 3 explain what the certificates are and what they are for.",
|
||||
"Chapters 4 and 5 explain naming and DNS delivery.",
|
||||
"Chapter 6 ties the whole estate back to operational reality.",
|
||||
"The appendices contain the detailed catalogue and the full inventory.",
|
||||
"Chapters 2 and 3 explain what the current certificates are and what they are for.",
|
||||
"Chapter 4 explains the historical lifecycle and splits red flags into current versus fixed-in-the-past.",
|
||||
"Chapters 5 and 6 explain naming and DNS delivery.",
|
||||
"Chapter 7 ties the whole estate back to operational reality.",
|
||||
"The appendices contain the detailed catalogue, the historical red-flag detail, and the full inventory.",
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -748,6 +1128,73 @@ def render_latex(args: argparse.Namespace, report: dict[str, object]) -> None:
|
|||
]
|
||||
)
|
||||
|
||||
lines.append(r"\section{Historical Renewal, Drift, and Red Flags}")
|
||||
add_summary(
|
||||
[
|
||||
f"Historical leaf certificates in scope: {historical_count}, of which {historical_current_count} are still currently valid.",
|
||||
f"Subject CN values with more than one certificate over time: {repeated_cn_count}.",
|
||||
f"Renewal asset lineages with normal rollover overlap below 50 days: {assessment.normal_reissuance_assets}.",
|
||||
f"Current overlap red flags at 50 days or more: {len(assessment.overlap_current_rows)}.",
|
||||
f"Past-only overlap red flags now fixed: {len(assessment.overlap_past_rows)}.",
|
||||
f"Current Subject DN drift / CA lineage drift / SAN drift counts: {len(assessment.dn_current_rows)} / {len(assessment.vendor_current_rows)} / {len(assessment.san_current_rows)}.",
|
||||
f"Past-only Subject DN drift / CA lineage drift / SAN drift counts: {len(assessment.dn_past_rows)} / {len(assessment.vendor_past_rows)} / {len(assessment.san_past_rows)}.",
|
||||
]
|
||||
)
|
||||
lines.append(
|
||||
r"This chapter is the historical control layer for the whole publication. A normal renewal reissues what is essentially the same certificate asset with a new key and a new validity span, and predecessor and successor overlap only briefly. In this monograph, anything below fifty days of overlap is treated as normal. Fifty days or more is treated as a red flag. COMODO and Sectigo are treated as one CA lineage from the outset, so movement between those names is not counted as lineage drift here."
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
r"\subsection{Current Red-Flag Inventory}",
|
||||
]
|
||||
)
|
||||
if assessment.current_red_flag_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedright\arraybackslash}p{0.28\linewidth} >{\raggedright\arraybackslash}p{0.26\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Score & Certs & Current & Flags & Issuer Mix \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.current_red_flag_rows[:25]:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.score} & {row.certificate_count} & {row.current_certificate_count} & {latex_escape(row.flags)} & {latex_escape(row.notes)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No current red flags were found under the configured rules.")
|
||||
lines.append(r"\subsection{Past Red Flags Now Fixed}")
|
||||
if assessment.past_red_flag_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedright\arraybackslash}p{0.28\linewidth} >{\raggedright\arraybackslash}p{0.26\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Score & Certs & Current & Flags & Issuer Mix \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.past_red_flag_rows[:25]:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.score} & {row.certificate_count} & {row.current_certificate_count} & {latex_escape(row.flags)} & {latex_escape(row.notes)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No past-only red flags were found under the configured rules.")
|
||||
lines.extend(
|
||||
[
|
||||
r"\subsection{What The Historical Red Flags Mean}",
|
||||
rf"Overlap red flags mean predecessor and successor certificates inside the same renewal asset lineage coexist for fifty days or more. Current cases: {len(assessment.overlap_current_rows)}. Past-only fixed cases: {len(assessment.overlap_past_rows)}.",
|
||||
rf"Subject-DN drift means the same Subject CN appears under more than one full Subject DN. Current cases: {len(assessment.dn_current_rows)}. Past-only fixed cases: {len(assessment.dn_past_rows)}.",
|
||||
rf"CA-lineage drift means the same Subject CN appears under more than one CA lineage after collapsing COMODO and Sectigo together. Current cases: {len(assessment.vendor_current_rows)}. Past-only fixed cases: {len(assessment.vendor_past_rows)}.",
|
||||
rf"SAN drift means the same Subject CN appears with more than one SAN profile. Current cases: {len(assessment.san_current_rows)}. Past-only fixed cases: {len(assessment.san_past_rows)}.",
|
||||
rf"Exact issuer-name changes also exist for {len(assessment.issuer_rows)} Subject CN values, but these are supporting context rather than first-order lineage red flags.",
|
||||
r"\subsection{Historical Step Changes}",
|
||||
rf"Top issuance start dates are {latex_escape(', '.join(f'{row.start_day} ({row.certificate_count})' for row in assessment.day_rows[:6]))}.",
|
||||
rf"Strong step weeks are {latex_escape(', '.join(f'{row.week_start} ({row.certificate_count} vs prior avg {row.prior_eight_week_avg})' for row in assessment.week_rows[:4]) or 'none')}.",
|
||||
]
|
||||
)
|
||||
|
||||
lines.append(r"\section{Naming Architecture}")
|
||||
add_summary(
|
||||
[
|
||||
|
|
@ -843,22 +1290,213 @@ def render_latex(args: argparse.Namespace, report: dict[str, object]) -> None:
|
|||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
|
||||
if dual_items:
|
||||
lines.extend(
|
||||
[
|
||||
r"\section{Historical Red-Flag Detail}",
|
||||
r"This appendix keeps the detailed historical evidence inside the monograph so that the reader does not need a second report.",
|
||||
r"\subsection{Current Red-Flag Inventory}",
|
||||
]
|
||||
)
|
||||
if assessment.current_red_flag_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\section{Detailed Dual-EKU Catalogue}",
|
||||
r"This appendix keeps the complete dual-EKU evidence available without letting the minority case dominate the main analytical chapter.",
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.38\linewidth} >{\raggedright\arraybackslash}p{0.12\linewidth} >{\raggedright\arraybackslash}p{0.12\linewidth} >{\raggedright\arraybackslash}p{0.18\linewidth} >{\raggedleft\arraybackslash}p{0.08\linewidth}}",
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedright\arraybackslash}p{0.28\linewidth} >{\raggedright\arraybackslash}p{0.26\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Valid From & Valid To & Issuer & DNS SANs \\",
|
||||
r"Subject CN & Score & Certs & Current & Flags & Issuer Mix \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for item in dual_items:
|
||||
for row in assessment.current_red_flag_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(item.subject_cn)} & {latex_escape(item.valid_from_utc[:10])} & {latex_escape(item.valid_to_utc[:10])} & {latex_escape(short_issuer(item.issuer_name))} & {len(item.san_dns_names)} \\"
|
||||
rf"{latex_escape(row.subject_cn)} & {row.score} & {row.certificate_count} & {row.current_certificate_count} & {latex_escape(row.flags)} & {latex_escape(row.notes)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No current red flags were found.")
|
||||
lines.append(r"\subsection{Past Red-Flag Inventory Now Fixed}")
|
||||
if assessment.past_red_flag_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedright\arraybackslash}p{0.28\linewidth} >{\raggedright\arraybackslash}p{0.26\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Score & Certs & Current & Flags & Issuer Mix \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.past_red_flag_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.score} & {row.certificate_count} & {row.current_certificate_count} & {latex_escape(row.flags)} & {latex_escape(row.notes)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No past-only red flags were found.")
|
||||
lines.append(r"\subsection{Current Overlap Red Flags}")
|
||||
if assessment.overlap_current_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.14\linewidth} >{\raggedright\arraybackslash}p{0.12\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.08\linewidth} >{\raggedleft\arraybackslash}p{0.08\linewidth} >{\raggedright\arraybackslash}p{0.13\linewidth} >{\raggedright\arraybackslash}p{0.24\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Lineage & Asset Certs & Current & Max Concurrent & Max Overlap Days & Class & Asset Details \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.overlap_current_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {latex_escape(row.lineage)} & {row.asset_variant_count} & {row.current_certificate_count} & {row.max_concurrent} & {row.max_overlap_days} & {latex_escape(row.overlap_class)} & {latex_escape(row.details)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No current overlap red flags were found.")
|
||||
lines.append(r"\subsection{Past Overlap Red Flags Now Fixed}")
|
||||
if assessment.overlap_past_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.14\linewidth} >{\raggedright\arraybackslash}p{0.12\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.08\linewidth} >{\raggedleft\arraybackslash}p{0.08\linewidth} >{\raggedright\arraybackslash}p{0.13\linewidth} >{\raggedright\arraybackslash}p{0.24\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Lineage & Asset Certs & Current & Max Concurrent & Max Overlap Days & Class & Asset Details \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.overlap_past_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {latex_escape(row.lineage)} & {row.asset_variant_count} & {row.current_certificate_count} & {row.max_concurrent} & {row.max_overlap_days} & {latex_escape(row.overlap_class)} & {latex_escape(row.details)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No past overlap red flags were found.")
|
||||
lines.append(r"\subsection{Current Subject-DN Drift}")
|
||||
if assessment.dn_current_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.09\linewidth} >{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedright\arraybackslash}p{0.29\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Certs & Current & Distinct Subject DNs & Issuer Families & Subject DN Samples \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.dn_current_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.certificate_count} & {row.current_certificate_count} & {row.distinct_value_count} & {latex_escape(row.issuer_families)} & {latex_escape(row.details)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No current Subject-DN drift was found.")
|
||||
lines.append(r"\subsection{Past Subject-DN Drift Now Fixed}")
|
||||
if assessment.dn_past_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.09\linewidth} >{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedright\arraybackslash}p{0.29\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Certs & Current & Distinct Subject DNs & Issuer Families & Subject DN Samples \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.dn_past_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.certificate_count} & {row.current_certificate_count} & {row.distinct_value_count} & {latex_escape(row.issuer_families)} & {latex_escape(row.details)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No past-only Subject-DN drift was found.")
|
||||
lines.append(r"\subsection{Current CA-Lineage Drift}")
|
||||
if assessment.vendor_current_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.08\linewidth} >{\raggedright\arraybackslash}p{0.18\linewidth} >{\raggedright\arraybackslash}p{0.32\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Certs & Current & Distinct Lineages & Lineage Mix & Lineages Seen \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.vendor_current_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.certificate_count} & {row.current_certificate_count} & {row.distinct_value_count} & {latex_escape(row.issuer_families)} & {latex_escape(row.details)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No current CA-lineage drift was found.")
|
||||
lines.append(r"\subsection{Past CA-Lineage Drift Now Fixed}")
|
||||
if assessment.vendor_past_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.20\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.08\linewidth} >{\raggedright\arraybackslash}p{0.18\linewidth} >{\raggedright\arraybackslash}p{0.32\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Certs & Current & Distinct Lineages & Lineage Mix & Lineages Seen \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.vendor_past_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.certificate_count} & {row.current_certificate_count} & {row.distinct_value_count} & {latex_escape(row.issuer_families)} & {latex_escape(row.details)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No past-only CA-lineage drift was found.")
|
||||
lines.append(r"\subsection{Current SAN Drift}")
|
||||
if assessment.san_current_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.16\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedright\arraybackslash}p{0.18\linewidth} >{\raggedright\arraybackslash}p{0.25\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Certs & Current & Profiles & Stable & Variable & Delta Pattern & Representative Delta \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.san_current_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.certificate_count} & {row.current_certificate_count} & {row.distinct_san_profiles} & {row.stable_entries} & {row.variable_entries} & {latex_escape(row.delta_pattern)} & {latex_escape(row.representative_delta)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No current SAN drift was found.")
|
||||
lines.append(r"\subsection{Past SAN Drift Now Fixed}")
|
||||
if assessment.san_past_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.16\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.06\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedleft\arraybackslash}p{0.07\linewidth} >{\raggedright\arraybackslash}p{0.18\linewidth} >{\raggedright\arraybackslash}p{0.25\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Subject CN & Certs & Current & Profiles & Stable & Variable & Delta Pattern & Representative Delta \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.san_past_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.subject_cn)} & {row.certificate_count} & {row.current_certificate_count} & {row.distinct_san_profiles} & {row.stable_entries} & {row.variable_entries} & {latex_escape(row.delta_pattern)} & {latex_escape(row.representative_delta)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No past-only SAN drift was found.")
|
||||
lines.append(r"\subsection{Historic Start Dates}")
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.13\linewidth} >{\raggedleft\arraybackslash}p{0.09\linewidth} >{\raggedright\arraybackslash}p{0.43\linewidth} >{\raggedright\arraybackslash}p{0.27\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Start Day & Certificates & Top Subject CNs & Top Issuer Families \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.day_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.start_day)} & {row.certificate_count} & {latex_escape(row.top_subjects)} & {latex_escape(row.top_issuers)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
lines.append(r"\subsection{Historic Step Weeks}")
|
||||
if assessment.week_rows:
|
||||
lines.extend(
|
||||
[
|
||||
r"\begin{longtable}{>{\raggedright\arraybackslash}p{0.13\linewidth} >{\raggedleft\arraybackslash}p{0.08\linewidth} >{\raggedleft\arraybackslash}p{0.10\linewidth} >{\raggedright\arraybackslash}p{0.35\linewidth} >{\raggedright\arraybackslash}p{0.24\linewidth}}",
|
||||
r"\toprule",
|
||||
r"Week Start & Certs & Prior 8-Week Avg & Top Subject CNs & Top Issuer Families \\",
|
||||
r"\midrule",
|
||||
]
|
||||
)
|
||||
for row in assessment.week_rows:
|
||||
lines.append(
|
||||
rf"{latex_escape(row.week_start)} & {row.certificate_count} & {latex_escape(row.prior_eight_week_avg)} & {latex_escape(row.top_subjects)} & {latex_escape(row.top_issuers)} \\"
|
||||
)
|
||||
lines.extend([r"\bottomrule", r"\end{longtable}"])
|
||||
else:
|
||||
lines.append(r"No step weeks met the threshold.")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
|
|
@ -874,9 +1512,10 @@ def render_latex(args: argparse.Namespace, report: dict[str, object]) -> None:
|
|||
def main() -> int:
|
||||
args = parse_args()
|
||||
report = ct_master_report.summarize_for_report(args)
|
||||
assessment = ct_lineage_report.build_assessment(build_history_args(args))
|
||||
render_appendix_inventory(args, report)
|
||||
render_markdown(args, report)
|
||||
render_latex(args, report)
|
||||
render_markdown(args, report, assessment)
|
||||
render_latex(args, report, assessment)
|
||||
if not args.skip_pdf:
|
||||
ct_scan.compile_latex_to_pdf(args.latex_output, args.pdf_output, args.pdf_engine)
|
||||
if not args.quiet:
|
||||
|
|
|
|||
Loading…
Reference in a new issue