-
Notifications
You must be signed in to change notification settings - Fork 47
Expand file tree
/
Copy pathdiff.m
More file actions
148 lines (125 loc) · 4.7 KB
/
Copy pathdiff.m
File metadata and controls
148 lines (125 loc) · 4.7 KB
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
//
// Diff.m
// KnockKnock
//
// Created by Patrick Wardle on 12/15/25.
// Copyright © 2025 Objective-See. All rights reserved.
//
#include "diff.h"
//generate key for item comparison
// note: items come from an untrusted (user-writable) file, so every value is type-checked
NSString* keyForItem(NSDictionary* item)
{
//command item? use command + file
if( (YES == [item[@"command"] isKindOfClass:[NSString class]]) &&
([item[@"command"] length] > 0) )
{
return [NSString stringWithFormat:@"%@|%@",
item[@"command"], [item[@"file"] isKindOfClass:[NSString class]] ? item[@"file"] : @""];
}
//default: use path
// (must be a string, else no key -> item is skipped)
return [item[@"path"] isKindOfClass:[NSString class]] ? item[@"path"] : nil;
}
//check if item changed (compare hashes/signatures)
BOOL itemChanged(NSDictionary* prevItem, NSDictionary* currentItem)
{
//fields to compare
NSArray* fields = @[@"hashes", @"signature(s)", @"name", @"plist", @"command"];
for(NSString* field in fields)
{
id prevValue = prevItem[field];
id currentValue = currentItem[field];
if(nil == prevValue && nil == currentValue) continue;
if(![prevValue isEqual:currentValue]) return YES;
}
return NO;
}
//format item for display
// note: '%@' is safe for any type, but 'length' is not, hence the type check
NSString* formatItem(NSDictionary* item)
{
//command item?
if( (YES == [item[@"command"] isKindOfClass:[NSString class]]) &&
([item[@"command"] length] > 0) )
{
return [NSString stringWithFormat:@"%@ (%@)", item[@"command"], item[@"file"] ?: @"unknown"];
}
//default: name + path
return [NSString stringWithFormat:@"%@ (%@)", item[@"name"] ?: @"unknown", item[@"path"] ?: @"unknown"];
}
//compare two scans, return diff string (nil on error)
NSString* diffScans(NSDictionary* prevScan, NSDictionary* currentScan)
{
//sanity check
if(![prevScan isKindOfClass:[NSDictionary class]] ||
![currentScan isKindOfClass:[NSDictionary class]])
{
return nil;
}
NSMutableString* diff = [NSMutableString string];
//get all categories
NSMutableSet* allCategories = [NSMutableSet setWithArray:prevScan.allKeys];
[allCategories addObjectsFromArray:currentScan.allKeys];
for(NSString* category in allCategories)
{
//categories must map to arrays (of dictionaries)
// ->anything else (from an untrusted file) is treated as empty
NSArray* prevItems = [prevScan[category] isKindOfClass:[NSArray class]] ? prevScan[category] : @[];
NSArray* currentItems = [currentScan[category] isKindOfClass:[NSArray class]] ? currentScan[category] : @[];
//build lookups
NSMutableDictionary* prevLookup = [NSMutableDictionary dictionary];
for(NSDictionary* item in prevItems)
{
if(![item isKindOfClass:[NSDictionary class]]) continue;
NSString* key = keyForItem(item);
if(key) prevLookup[key] = item;
}
NSMutableDictionary* currentLookup = [NSMutableDictionary dictionary];
for(NSDictionary* item in currentItems)
{
if(![item isKindOfClass:[NSDictionary class]]) continue;
NSString* key = keyForItem(item);
if(key) currentLookup[key] = item;
}
NSMutableString* categoryDiff = [NSMutableString string];
//removed (in prev but not current)
for(NSString* key in prevLookup)
{
if(nil == currentLookup[key])
{
[categoryDiff appendFormat:@" - %@\r\n", formatItem(prevLookup[key])];
}
}
//added (in current but not prev)
for(NSString* key in currentLookup)
{
if(nil == prevLookup[key])
{
[categoryDiff appendFormat:@" + %@\r\n", formatItem(currentLookup[key])];
}
}
//changed (in both but different)
for(NSString* key in currentLookup)
{
if(nil != prevLookup[key])
{
if(itemChanged(prevLookup[key], currentLookup[key]))
{
[categoryDiff appendFormat:@" ~ %@\r\n", formatItem(currentLookup[key])];
}
}
}
//any diffs in this category?
if(categoryDiff.length > 0)
{
[diff appendFormat:@"%@:\r\n%@\r\n", category, categoryDiff];
}
}
//no changes?
if(0 == diff.length)
{
return NSLocalizedString(@"No Changes Detected", @"No Changes Detected");
}
return diff;
}