summaryrefslogtreecommitdiff
path: root/DomainDig/DomainDig/AuditExporter.swift
blob: 0d9a09872f5bb7a78521de282989f3a77f9ca82e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import Foundation

#if canImport(UIKit)
import UIKit
#endif

enum AuditExportFormat: String, CaseIterable, Identifiable {
    case pdf
    case json
    case markdown

    var id: String { rawValue }

    var fileExtension: String { rawValue == "markdown" ? "md" : rawValue }
}

enum AuditExporter {
    static func data(for session: AuditSession, format: AuditExportFormat) throws -> Data {
        switch format {
        case .json:
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
            encoder.dateEncodingStrategy = .iso8601
            return try encoder.encode(session)
        case .markdown:
            return Data(markdown(for: session).utf8)
        case .pdf:
            return try pdfData(for: session)
        }
    }

    static func markdown(for session: AuditSession) -> String {
        var lines: [String] = [
            "# DomainDig Audit",
            "",
            "- Domain: \(session.domain)",
            "- Review Date: \(session.createdAt.formatted(date: .abbreviated, time: .shortened))",
            "- Reviewer: \(session.reviewer)",
            "- Status: \(session.status.title)",
            "- Evidence Captured: \(session.evidence.capturedAt.formatted(date: .abbreviated, time: .shortened))",
            "- Checklist Progress: \(session.completedChecklistCount)/\(session.checklist.count)",
            ""
        ]

        if !session.notes.isEmpty {
            lines.append("## Summary Notes")
            lines.append(session.notes)
            lines.append("")
        }

        lines.append("## Findings Summary")
        if session.findings.isEmpty {
            lines.append("No findings recorded.")
        } else {
            for finding in session.findings {
                lines.append("- [\(finding.severity.title)] \(finding.title) (\(finding.status.title))")
            }
        }
        lines.append("")

        lines.append("## Key Risks")
        if session.keyRisks.isEmpty {
            lines.append("No medium or high-severity risks recorded.")
        } else {
            for finding in session.keyRisks {
                lines.append("- \(finding.title): \(finding.summary)")
            }
        }
        lines.append("")

        lines.append("## Checklist")
        for item in session.checklist {
            lines.append("- [\(item.isComplete ? "x" : " ")] \(item.title): \(item.detail)")
        }
        lines.append("")

        lines.append("## Findings")
        if session.findings.isEmpty {
            lines.append("No findings recorded.")
        } else {
            for finding in session.findings {
                lines.append("### \(finding.title)")
                lines.append("- Severity: \(finding.severity.title)")
                lines.append("- Status: \(finding.status.title)")
                if !finding.checklistAreas.isEmpty {
                    lines.append("- Checklist Areas: \(finding.checklistAreas.map(\.title).joined(separator: ", "))")
                }
                lines.append("- Summary: \(finding.summary)")
                if !finding.evidenceReferences.isEmpty {
                    lines.append("- Evidence: \(finding.evidenceReferences.joined(separator: " | "))")
                }
                if !finding.notes.isEmpty {
                    lines.append("- Notes: \(finding.notes)")
                }
                lines.append("")
            }
        }

        lines.append("## Evidence Snapshot")
        lines.append("- Availability: \(availabilityTitle(session.evidence.report.availability))")
        lines.append("- Risk Score: \(session.evidence.report.riskAssessment.score) (\(session.evidence.report.riskAssessment.level.title))")
        lines.append("- TLS Status: \(session.evidence.report.web.tlsStatus)")
        lines.append("- Final URL: \(session.evidence.report.web.finalURL ?? "Unavailable")")
        lines.append("- Primary IP: \(session.evidence.report.dns.primaryIP ?? "Unavailable")")
        lines.append("- Reachability: \(session.evidence.report.network.reachabilitySummary)")
        lines.append("- Ownership: \(session.evidence.report.ownership?.registrar ?? "Unavailable")")
        lines.append("- Historical Context Snapshots: \(session.evidence.historicalContext.count)")

        return lines.joined(separator: "\n")
    }

    private static func availabilityTitle(_ status: DomainAvailabilityStatus) -> String {
        switch status {
        case .available:
            return "Available"
        case .registered:
            return "Registered"
        case .unknown:
            return "Unknown"
        }
    }

    private static func pdfData(for session: AuditSession) throws -> Data {
        let markdown = markdown(for: session)
        #if canImport(UIKit)
        let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 612, height: 792))
        return renderer.pdfData { context in
            let lines = markdown.components(separatedBy: .newlines)
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.lineBreakMode = .byWordWrapping
            let attributes: [NSAttributedString.Key: Any] = [
                .font: UIFont.monospacedSystemFont(ofSize: 11, weight: .regular),
                .paragraphStyle: paragraphStyle
            ]

            var yOffset: CGFloat = 36
            context.beginPage()

            for line in lines {
                if yOffset > 744 {
                    context.beginPage()
                    yOffset = 36
                }

                let renderedLine = NSString(string: line.isEmpty ? " " : line)
                renderedLine.draw(
                    in: CGRect(x: 36, y: yOffset, width: 540, height: 22),
                    withAttributes: attributes
                )
                yOffset += 16
            }
        }
        #else
        return Data(markdown.utf8)
        #endif
    }
}