krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v5.0.0: DomainDig/DomainViewModel+Audit.swift · raw
1import Foundation
2import SwiftUI
3
4/// Audit review surface of `DomainViewModel`: reading audit sessions and their
5/// timeline, and mutating a session's status, notes, checklist, and findings.
6///
7/// `startAudit(for:)` deliberately stays on the main type — it drives a live
8/// inspection to seed the session, so it belongs with the inspection pipeline
9/// until that is extracted. Everything here operates purely on `auditSessions`
10/// and persists through `persistAuditSessions()`.
11extension DomainViewModel {
12 func audits(for domain: String) -> [AuditSession] {
13 auditSessions
14 .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
15 .sorted { $0.createdAt > $1.createdAt }
16 }
17
18 func auditSession(withID id: UUID) -> AuditSession? {
19 auditSessions.first(where: { $0.id == id })
20 }
21
22 func auditTimeline(for domain: String) -> [AuditTimelinePoint] {
23 let sessions = audits(for: domain).sorted { $0.createdAt > $1.createdAt }
24 return sessions.map { session in
25 let repeatedIssues = sessions
26 .filter { $0.id != session.id }
27 .flatMap(\.findings)
28 .map { $0.title.lowercased() }
29 let repeatedIssueCount = session.findings.filter {
30 repeatedIssues.contains($0.title.lowercased())
31 }.count
32
33 return AuditTimelinePoint(
34 id: session.id,
35 sessionID: session.id,
36 domain: session.domain,
37 createdAt: session.createdAt,
38 status: session.status,
39 findingCount: session.findings.count,
40 openHighSeverityCount: session.findings.filter { $0.severity == .high && $0.status != .resolved }.count,
41 repeatedIssueCount: repeatedIssueCount
42 )
43 }
44 }
45
46 func updateAuditStatus(_ status: AuditStatus, sessionID: UUID) {
47 guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
48 auditSessions[index].status = status
49 persistAuditSessions()
50 }
51
52 func updateAuditNotes(_ notes: String, sessionID: UUID) {
53 guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
54 auditSessions[index].notes = notes.trimmingCharacters(in: .whitespacesAndNewlines)
55 persistAuditSessions()
56 }
57
58 func toggleAuditChecklistItem(sessionID: UUID, itemID: UUID) {
59 guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }),
60 let itemIndex = auditSessions[sessionIndex].checklist.firstIndex(where: { $0.id == itemID }) else {
61 return
62 }
63
64 auditSessions[sessionIndex].checklist[itemIndex].isComplete.toggle()
65 auditSessions[sessionIndex].checklist[itemIndex].completedAt = auditSessions[sessionIndex].checklist[itemIndex].isComplete ? Date() : nil
66 persistAuditSessions()
67 }
68
69 func addAuditFinding(
70 sessionID: UUID,
71 title: String,
72 severity: AuditFindingSeverity,
73 summary: String,
74 evidenceReferences: [String],
75 notes: String,
76 checklistAreas: [AuditChecklistArea]
77 ) {
78 guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
79 let finding = AuditFinding(
80 title: title,
81 severity: severity,
82 summary: summary,
83 evidenceReferences: evidenceReferences,
84 notes: notes,
85 status: .open,
86 checklistAreas: checklistAreas
87 )
88 auditSessions[index].findings.insert(finding, at: 0)
89 persistAuditSessions()
90 }
91
92 func updateAuditFinding(_ finding: AuditFinding, sessionID: UUID) {
93 guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }),
94 let findingIndex = auditSessions[sessionIndex].findings.firstIndex(where: { $0.id == finding.id }) else {
95 return
96 }
97
98 var updatedFinding = finding
99 updatedFinding.updatedAt = Date()
100 auditSessions[sessionIndex].findings[findingIndex] = updatedFinding
101 persistAuditSessions()
102 }
103
104 func removeAuditFindings(at offsets: IndexSet, sessionID: UUID) {
105 guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
106 auditSessions[sessionIndex].findings.remove(atOffsets: offsets)
107 persistAuditSessions()
108 }
109
110 func exportAuditData(sessionID: UUID, format: AuditExportFormat) -> Data? {
111 guard let session = auditSession(withID: sessionID) else { return nil }
112 return try? AuditExporter.data(for: session, format: format)
113 }
114}