krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.1: 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}