Repository navigation
Expand file tree
/
Copy pathscanner.php
More file actions
191 lines (156 loc) · 6.96 KB
/
Copy pathscanner.php
File metadata and controls
191 lines (156 loc) · 6.96 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<?php
/**
* DNS Sync Validator — Background Scanner
*
* Run via api.php which launches: nohup php scanner.php >> data/scanner.log 2>&1 &
* Do NOT run directly through the web server.
*
* For each unique (domain, host, type) record group in domain_list.csv, this
* script queries both DNS servers and records whether they return identical
* results. Results are written incrementally to data/results.json so the
* dashboard can display live progress.
*
* To stop a running scan gracefully, create the file data/stop_signal or use
* the Stop button in the dashboard which calls api.php?action=stop.
*
* Copyright (c) 2026 Brad Boegler <bradthx@gmail.com>
* GNU General Public License v3.0
*/
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/lib/CsvParser.php';
require_once __DIR__ . '/lib/DnsChecker.php';
require_once __DIR__ . '/lib/ResultStore.php';
// Never time out regardless of how long the scan runs
set_time_limit(0);
ignore_user_abort(true);
// Ensure data directory exists
if (!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
// Write PID file so api.php can verify the process is alive
file_put_contents(PID_FILE, getmypid());
// Remove any stale stop signal from a previous run
if (file_exists(STOP_SIGNAL_FILE)) {
unlink(STOP_SIGNAL_FILE);
}
// ─── Initialization ───────────────────────────────────────────────────────────
$scanId = date('Ymd_His');
$issuesFile = DATA_DIR . '/issues_' . $scanId . '.txt';
echo "[{$scanId}] Scanner starting (PID " . getmypid() . ")\n";
// Count total lines once for the progress bar denominator
$parser = new CsvParser(CSV_FILE);
$totalRecords = $parser->countLines();
echo "[{$scanId}] Total CSV records: {$totalRecords}\n";
// Initialize result state and write immediately so the dashboard shows activity
$results = ResultStore::blank();
$results['scan_id'] = $scanId;
$results['status'] = 'running';
$results['total_records'] = $totalRecords;
$results['started_at'] = date('Y-m-d H:i:s');
$results['issues_file'] = basename($issuesFile);
ResultStore::write($results);
// Open the per-run issues file for appending domain names with mismatches
$issuesFh = fopen($issuesFile, 'w');
if ($issuesFh === false) {
echo "[{$scanId}] ERROR: Cannot open issues file: {$issuesFile}\n";
exit(1);
}
// ─── Per-domain tracking ─────────────────────────────────────────────────────
$groupsProcessed = 0;
$processedLines = 0;
$currentDomain = null;
$domainAllMatched = true; // tracks if every group for the current domain matched
// ─── Main Scan Loop ───────────────────────────────────────────────────────────
foreach ($parser->streamGroups() as $group) {
// Check for stop signal before starting each group
if (file_exists(STOP_SIGNAL_FILE)) {
echo "[{$scanId}] Stop signal received. Halting after {$groupsProcessed} groups.\n";
$results['status'] = 'stopped';
break;
}
$fqdn = DnsChecker::buildFqdn($group['domain'], $group['host']);
$type = $group['type'];
// Query both authoritative servers
$primaryValues = DnsChecker::query(PRIMARY_DNS_SERVER, $fqdn, $type);
$secondaryValues = DnsChecker::query(SECONDARY_DNS_SERVER, $fqdn, $type);
// A match means both servers return the exact same set of values
$match = DnsChecker::compare($primaryValues, $secondaryValues);
// Update counters
$groupsProcessed++;
$processedLines += $group['lines'];
$results['total_groups'] = $groupsProcessed;
$results['processed_lines'] = $processedLines;
if (!isset($results['by_type'][$type])) {
$results['by_type'][$type] = ['matched' => 0, 'unmatched' => 0];
}
if ($match) {
$results['matched']++;
$results['by_type'][$type]['matched']++;
} else {
$results['unmatched']++;
$results['by_type'][$type]['unmatched']++;
}
// Track per-domain state for the "last 25 domains" lists
if ($group['domain'] !== $currentDomain) {
if ($currentDomain !== null) {
flushDomain($currentDomain, $domainAllMatched, $results, $issuesFh);
}
$currentDomain = $group['domain'];
$domainAllMatched = true;
}
if (!$match) {
$domainAllMatched = false;
}
// Throttle to avoid hammering iterative DNS servers
if (THROTTLE_USEC > 0) {
usleep(THROTTLE_USEC);
}
// Flush results to disk periodically
if ($groupsProcessed % FLUSH_EVERY === 0) {
ResultStore::write($results);
$pct = $totalRecords > 0 ? round($processedLines / $totalRecords * 100, 1) : 0;
echo "[{$scanId}] {$groupsProcessed} groups | {$pct}% | matched: {$results['matched']} | unmatched: {$results['unmatched']}\n";
}
}
// ─── Finalization ─────────────────────────────────────────────────────────────
// Flush the last domain
if ($currentDomain !== null) {
flushDomain($currentDomain, $domainAllMatched, $results, $issuesFh);
}
if ($results['status'] !== 'stopped') {
$results['status'] = 'complete';
}
ResultStore::write($results);
fclose($issuesFh);
// Remove PID file now that we are done
if (file_exists(PID_FILE)) {
unlink(PID_FILE);
}
echo "[{$scanId}] Scan {$results['status']}. Groups: {$groupsProcessed} | Matched: {$results['matched']} | Unmatched: {$results['unmatched']}\n";
// ─── Helper Functions ─────────────────────────────────────────────────────────
/**
* Record a completed domain into the appropriate recent-domains list.
* Domains with any mismatch are also written to the issues file.
*
* @param string $domain The domain name
* @param bool $allMatched True if every record group for this domain matched
* @param array $results Results array passed by reference
* @param resource $issuesFh Open file handle to the issues text file
*/
function flushDomain(string $domain, bool $allMatched, array &$results, $issuesFh): void
{
if ($allMatched) {
$results['domains_matched']++;
array_unshift($results['recent_matched_domains'], $domain);
$results['recent_matched_domains'] = array_slice(
$results['recent_matched_domains'], 0, 25
);
} else {
$results['domains_unmatched']++;
array_unshift($results['recent_unmatched_domains'], $domain);
$results['recent_unmatched_domains'] = array_slice(
$results['recent_unmatched_domains'], 0, 25
);
fwrite($issuesFh, $domain . "\n");
}
}