Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions SecVF.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
TUIT002TUIT002TUIT002002 /* TacticalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = TUIT001TUIT001TUIT001001 /* TacticalUITests.swift */; };
NPT002NPT002NPT002002 /* NetworkPeersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = NPT001NPT001NPT001001 /* NetworkPeersTests.swift */; };
PFPT002PFPT002PFPT002002 /* PacketFilterPresetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PFPT001PFPT001PFPT001001 /* PacketFilterPresetsTests.swift */; };
PFTT002PFTT002PFTT002002 /* PacketFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PFTT001PFTT001PFTT001001 /* PacketFilterTests.swift */; };
OVLT002OVLT002OVLT002002 /* OverlayViewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OVLT001OVLT001OVLT001001 /* OverlayViewsTests.swift */; };
LCT002LCT002LCT002002 /* LayoutConstantsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = LCT001LCT001LCT001001 /* LayoutConstantsTests.swift */; };
ACT002ACT002ACT002002 /* AppColorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACT001ACT001ACT001001 /* AppColorsTests.swift */; };
Expand Down Expand Up @@ -65,6 +66,7 @@
SPRK002SPRK002SPRK002002 /* SparklineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = SPRK001SPRK001SPRK001001 /* SparklineView.swift */; };
BIFS002BIFS002BIFS002002 /* BridgeInterfaceStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = BIFS001BIFS001BIFS001001 /* BridgeInterfaceStats.swift */; };
PFPR002PFPR002PFPR002002 /* PacketFilterPresets.swift in Sources */ = {isa = PBXBuildFile; fileRef = PFPR001PFPR001PFPR001001 /* PacketFilterPresets.swift */; };
PFLT002PFLT002PFLT002002 /* PacketFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = PFLT001PFLT001PFLT001001 /* PacketFilter.swift */; };
VCOV002VCOV002VCOV002002 /* VMConnectionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = VCOV001VCOV001VCOV001001 /* VMConnectionOverlayView.swift */; };
VTFO002VTFO002VTFO002002 /* VMTrafficFallOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = VTFO001VTFO001VTFO001001 /* VMTrafficFallOverlayView.swift */; };
THBT002THBT002THBT002002 /* TacticalHoverButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = THBT001THBT001THBT001001 /* TacticalHoverButton.swift */; };
Expand Down Expand Up @@ -125,6 +127,7 @@
TUIT001TUIT001TUIT001001 /* TacticalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalUITests.swift; sourceTree = "<group>"; };
NPT001NPT001NPT001001 /* NetworkPeersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkPeersTests.swift; sourceTree = "<group>"; };
PFPT001PFPT001PFPT001001 /* PacketFilterPresetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketFilterPresetsTests.swift; sourceTree = "<group>"; };
PFTT001PFTT001PFTT001001 /* PacketFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketFilterTests.swift; sourceTree = "<group>"; };
OVLT001OVLT001OVLT001001 /* OverlayViewsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayViewsTests.swift; sourceTree = "<group>"; };
LCT001LCT001LCT001001 /* LayoutConstantsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutConstantsTests.swift; sourceTree = "<group>"; };
ACT001ACT001ACT001001 /* AppColorsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColorsTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -155,6 +158,7 @@
SPRK001SPRK001SPRK001001 /* SparklineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparklineView.swift; sourceTree = "<group>"; };
BIFS001BIFS001BIFS001001 /* BridgeInterfaceStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeInterfaceStats.swift; sourceTree = "<group>"; };
PFPR001PFPR001PFPR001001 /* PacketFilterPresets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketFilterPresets.swift; sourceTree = "<group>"; };
PFLT001PFLT001PFLT001001 /* PacketFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketFilter.swift; sourceTree = "<group>"; };
VCOV001VCOV001VCOV001001 /* VMConnectionOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConnectionOverlayView.swift; sourceTree = "<group>"; };
VTFO001VTFO001VTFO001001 /* VMTrafficFallOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMTrafficFallOverlayView.swift; sourceTree = "<group>"; };
THBT001THBT001THBT001001 /* TacticalHoverButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalHoverButton.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -232,6 +236,7 @@
SPRK001SPRK001SPRK001001 /* SparklineView.swift */,
BIFS001BIFS001BIFS001001 /* BridgeInterfaceStats.swift */,
PFPR001PFPR001PFPR001001 /* PacketFilterPresets.swift */,
PFLT001PFLT001PFLT001001 /* PacketFilter.swift */,
VCOV001VCOV001VCOV001001 /* VMConnectionOverlayView.swift */,
VTFO001VTFO001VTFO001001 /* VMTrafficFallOverlayView.swift */,
THBT001THBT001THBT001001 /* TacticalHoverButton.swift */,
Expand Down Expand Up @@ -293,6 +298,7 @@
TUIT001TUIT001TUIT001001 /* TacticalUITests.swift */,
NPT001NPT001NPT001001 /* NetworkPeersTests.swift */,
PFPT001PFPT001PFPT001001 /* PacketFilterPresetsTests.swift */,
PFTT001PFTT001PFTT001001 /* PacketFilterTests.swift */,
OVLT001OVLT001OVLT001001 /* OverlayViewsTests.swift */,
LCT001LCT001LCT001001 /* LayoutConstantsTests.swift */,
ACT001ACT001ACT001001 /* AppColorsTests.swift */,
Expand Down Expand Up @@ -451,6 +457,7 @@
SPRK002SPRK002SPRK002002 /* SparklineView.swift in Sources */,
BIFS002BIFS002BIFS002002 /* BridgeInterfaceStats.swift in Sources */,
PFPR002PFPR002PFPR002002 /* PacketFilterPresets.swift in Sources */,
PFLT002PFLT002PFLT002002 /* PacketFilter.swift in Sources */,
VCOV002VCOV002VCOV002002 /* VMConnectionOverlayView.swift in Sources */,
VTFO002VTFO002VTFO002002 /* VMTrafficFallOverlayView.swift in Sources */,
THBT002THBT002THBT002002 /* TacticalHoverButton.swift in Sources */,
Expand Down Expand Up @@ -491,6 +498,7 @@
TUIT002TUIT002TUIT002002 /* TacticalUITests.swift in Sources */,
NPT002NPT002NPT002002 /* NetworkPeersTests.swift in Sources */,
PFPT002PFPT002PFPT002002 /* PacketFilterPresetsTests.swift in Sources */,
PFTT002PFTT002PFTT002002 /* PacketFilterTests.swift in Sources */,
OVLT002OVLT002OVLT002002 /* OverlayViewsTests.swift in Sources */,
LCT002LCT002LCT002002 /* LayoutConstantsTests.swift in Sources */,
ACT002ACT002ACT002002 /* AppColorsTests.swift in Sources */,
Expand Down
132 changes: 41 additions & 91 deletions SecVF/PacketAnalysisWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ class PacketAnalysisWindowController: NSWindowController, NSTableViewDataSource,
private var displayedPackets: [CapturedPacket] = []
private var selectedPacket: CapturedPacket?
private var currentFilter: String = ""
/// Pre-compiled form of `currentFilter`. Set once via `setCurrentFilter`
/// so per-packet evaluation doesn't re-parse the expression. `nil`
/// means "no filter" (every packet passes); a `PacketFilter` value
/// means "evaluate against this AST". A parse error also produces
/// `nil` (compileOrNil) so a malformed live-typed filter shows
/// everything rather than nothing — surfaces faster as obviously
/// wrong than a silent empty list.
private var compiledFilter: PacketFilter?
private var autoScroll: Bool = true

// PERFORMANCE: Batched packet updates to reduce UI redraws during high-traffic captures
Expand Down Expand Up @@ -449,7 +457,7 @@ class PacketAnalysisWindowController: NSWindowController, NSTableViewDataSource,
return
}
filterTextField.stringValue = filter
currentFilter = filter.lowercased()
setCurrentFilter(filter)
reloadPackets()
updateStatus()
showFilterInfo(for: selectedTitle)
Expand All @@ -461,33 +469,40 @@ class PacketAnalysisWindowController: NSWindowController, NSTableViewDataSource,
func applyPresetByTitle(_ title: String) {
guard let filter = PacketFilterPresets.filter(for: title) else { return }
filterTextField.stringValue = filter
currentFilter = filter.lowercased()
setCurrentFilter(filter)
reloadPackets()
updateStatus()
showFilterInfo(for: title)
}

private func showFilterInfo(for filterName: String) {
let infoMap: [String: String] = [
"Non-Apple DNS (Suspicious)": "DNS queries NOT to Apple/iCloud - potential C2 communication",
"Direct IP Connections (No DNS)": "TCP to raw IPs without DNS lookup - malware often does this",
"Non-Apple DNS (Suspicious)": "DNS queries NOT to Apple/iCloud potential C2 communication",
"Direct IP Connections (No DNS)": "TCP to raw IPs without DNS lookup malware often does this",
"Suspicious TLDs (.tk/.ml/.ga/.cf)": "Free TLDs commonly used by malware for C2 domains",
"Non-Browser HTTP (curl/wget/python)": "HTTP from non-browser tools - may be scripted malware",
"DNS Tunneling (Long Queries)": "Unusually long DNS names may hide exfiltrated data",
"HTTP (inspect for non-browser UA manually)": "Filter shows all HTTP; check User-Agent on each row for curl/wget/python — the parser doesn't read UA headers.",
"Non-Standard Ports": "TCP traffic on ports other than the four well-known commodity ports (22/53/80/443)",
"TCP (inspect for short-connection beacons manually)": "Filter shows all TCP; look for repeated short-duration connections to the same destination.",
"DNS Tunneling (Long Queries)": "DNS packets >100 bytes — unusually-long names may hide exfiltrated data",
"Large Outbound Transfers": "TCP packets >1000 bytes — bulk data movement",
"ICMP with Payload (Covert Channel)": "ICMP packets >64 bytes — payload-bearing ICMP may be a covert channel",
"HTTP (inspect for base64 payloads manually)": "Filter shows all HTTP; inspect each row's body for base64-encoded data.",
"All TLS / SSL": "All TLS / SSL traffic",
"TLS (inspect handshake/SNI/certs manually)": "Filter shows all TLS; inspect handshake records for SNI, certificate chain, and self-signed indicators.",
"TCP (inspect for SYN-flood patterns manually)": "Filter shows all TCP; multiple SYN packets to different ports indicates scanning.",
"ARP Requests (Host Discovery)": "ARP requests can indicate network reconnaissance",
"Port Scanning (SYN Flood)": "Multiple SYN packets to different ports = scanning",
"ICMP with Payload (Covert Channel)": "ICMP with data payload may be covert channel",
"TLS Without SNI (Hidden Dest)": "TLS without Server Name Indication hides destination",
"ICMP Echo (Ping Sweep)": "ICMP echo requests across many hosts indicate ping sweeping",
"SMB Enumeration": "SMB traffic + ports 445/139 — host/share enumeration",
"SSH Traffic": "SSH can be used for tunneling and lateral movement"
]

if let info = infoMap[filterName] {
NSLog("[PacketAnalysis] Filter applied: \(filterName) - \(info)")
NSLog("[PacketAnalysis] Filter applied: \(filterName) \(info)")
}
}

@objc private func applyFilter(_ sender: Any) {
currentFilter = filterTextField.stringValue.lowercased().trimmingCharacters(in: .whitespaces)
setCurrentFilter(filterTextField.stringValue)
reloadPackets()
updateStatus()
}
Expand Down Expand Up @@ -560,86 +575,21 @@ class PacketAnalysisWindowController: NSWindowController, NSTableViewDataSource,
}

private func passesFilter(_ packet: CapturedPacket) -> Bool {
guard !currentFilter.isEmpty else { return true }

let filter = currentFilter.lowercased()

// Handle "or" expressions - any term matching means pass
if filter.contains(" or ") {
let terms = filter.components(separatedBy: " or ")
for term in terms {
if matchesSingleTerm(packet, term: term.trimmingCharacters(in: .whitespaces)) {
return true
}
}
return false
}

// Handle "and" expressions - all terms must match
if filter.contains(" and ") {
let terms = filter.components(separatedBy: " and ")
for term in terms {
if !matchesSingleTerm(packet, term: term.trimmingCharacters(in: .whitespaces)) {
return false
}
}
return true
}

// Single term filter
return matchesSingleTerm(packet, term: filter)
}

private func matchesSingleTerm(_ packet: CapturedPacket, term: String) -> Bool {
let proto = packet.protocol.lowercased()
let info = packet.info.lowercased()

// Protocol filters
if term == "tcp" { return proto == "tcp" }
if term == "udp" { return proto == "udp" }
if term == "icmp" { return proto == "icmp" }
if term == "arp" { return proto == "arp" }
if term == "dns" { return proto == "dns" }
if term == "http" { return proto == "http" || info.contains("http") }
if term == "https" { return proto == "https" || info.contains("https") || info.contains(":443") }
if term == "ipv6" { return proto == "ipv6" }
if term == "ssh" { return info.contains(":22") || info.contains("ssh") }
if term == "smb" { return info.contains(":445") || info.contains("smb") }
if term == "afp" { return info.contains(":548") || info.contains("afp") }

// Port filters: "tcp 80", "tcp 443", "port 22"
if term.hasPrefix("tcp ") {
let port = term.replacingOccurrences(of: "tcp ", with: "")
return proto == "tcp" && (info.contains(":\(port)") || info.contains("→\(port)") || info.contains("->\(port)"))
}
if term.hasPrefix("udp ") {
let port = term.replacingOccurrences(of: "udp ", with: "")
return proto == "udp" && (info.contains(":\(port)") || info.contains("→\(port)") || info.contains("->\(port)"))
}
if term.hasPrefix("port ") {
let port = term.replacingOccurrences(of: "port ", with: "")
return info.contains(":\(port)") || info.contains("→\(port)") || info.contains("->\(port)")
}

// IP address filter
if term.contains("ip.addr") {
if let ipMatch = term.components(separatedBy: "==").last?.trimmingCharacters(in: .whitespaces) {
return packet.sourceIP == ipMatch || packet.destIP == ipMatch
}
}

// "not" prefix for exclusion
if term.hasPrefix("not ") {
let innerTerm = String(term.dropFirst(4))
return !matchesSingleTerm(packet, term: innerTerm)
}

// Text search in info field
if info.contains(term) || proto.contains(term) {
return true
}

return false
// No filter set OR filter failed to compile (treat as no filter
// so a malformed live-typed expression doesn't silently empty
// the table — the operator sees everything pass through and
// can spot the parse failure faster).
guard let compiled = compiledFilter else { return true }
return compiled.matches(packet)
}

/// Update the current filter string and recompile the AST. Call this
/// from every site that previously set `currentFilter` directly so
/// the compiled cache stays in sync.
private func setCurrentFilter(_ raw: String) {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
currentFilter = trimmed
compiledFilter = PacketFilter.compileOrNil(trimmed)
}

// MARK: - UI Updates
Expand Down
2 changes: 1 addition & 1 deletion SecVF/PacketCaptureManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Combine

// MARK: - Data Structures

struct CapturedPacket {
struct CapturedPacket: PacketLike {
let number: Int
let timestamp: Date
let relativeTime: Double
Expand Down
Loading
Loading