Updates to scanner

This commit is contained in:
Mark Gregory
2026-10-22 12:55:49 +01:00
parent 8a8061b1e8
commit 0c4a1625b0
6 changed files with 101 additions and 29 deletions
+4 -4
View File
@@ -168,8 +168,8 @@
GENERATE_INFOPLIST_FILE = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_KEY_CFBundleDisplayName = PHPGuard;
MARKETING_VERSION = 1.4.0;
CURRENT_PROJECT_VERSION = 5;
MARKETING_VERSION = 1.5.0;
CURRENT_PROJECT_VERSION = 6;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
MACOSX_DEPLOYMENT_TARGET = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard;
@@ -187,8 +187,8 @@
GENERATE_INFOPLIST_FILE = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_KEY_CFBundleDisplayName = PHPGuard;
MARKETING_VERSION = 1.4.0;
CURRENT_PROJECT_VERSION = 5;
MARKETING_VERSION = 1.5.0;
CURRENT_PROJECT_VERSION = 6;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
MACOSX_DEPLOYMENT_TARGET = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard;
+31 -1
View File
@@ -19,7 +19,37 @@ import SwiftUI
}
init() {
servers = load("servers.json") ?? []
results = load("results.json") ?? []
results = (load("results.json") ?? []).map { Self.normalized($0) }
}
private static func normalized(_ source: AuditResult) -> AuditResult {
var result = source
result.dockerPHP = source.dockerPHP.filter {
!$0.contains("OCI runtime exec failed") && !$0.contains("executable file not found")
}
result.findings.removeAll {
$0.title == "PHP runtime in Docker" &&
($0.detail.contains("OCI runtime exec failed") || $0.detail.contains("executable file not found"))
}
if result.dockerPHP.count != source.dockerPHP.count {
result.dockerAuditStatus = "Legacy scan: missing PHP executables excluded; rescan for structured Docker coverage"
}
return result
}
func importReport() {
let panel = NSOpenPanel()
panel.allowedContentTypes = [.json]
panel.allowsMultipleSelection = false
guard panel.runModal() == .OK, let url = panel.url else { return }
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let report = Self.normalized(try decoder.decode(AuditResult.self, from: Data(contentsOf: url)))
guard !results.contains(where: { $0.id == report.id }) else {
lastError = "This report has already been imported."
return
}
results.insert(report, at: 0)
} catch { lastError = "Could not import JSON report: \(error.localizedDescription)" }
}
private func load<T: Decodable>(_ name: String) -> T? {
guard let data = try? Data(contentsOf: folder.appendingPathComponent(name)) else { return nil }
+4 -1
View File
@@ -69,7 +69,10 @@ struct ContentView: View {
}
Text("Read-only PHP inventory, APT upgrade checks and running Docker container discovery. CVE matching and vendor backport verification remain unimplemented; no server is certified secure.")
.foregroundStyle(.secondary)
Button("Scan all servers") { store.scanAll() }.buttonStyle(.borderedProminent).disabled(store.servers.isEmpty)
HStack {
Button("Scan all servers") { store.scanAll() }.buttonStyle(.borderedProminent).disabled(store.servers.isEmpty)
Button("Import JSON scan") { store.importReport() }
}
Spacer()
}.padding(30).frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
+50 -15
View File
@@ -41,19 +41,23 @@ enum SSHScanner {
printf 'APT metadata unavailable\n'
fi
printf '===DOCKER===\n'
if command -v docker >/dev/null 2>&1; then
ids=$(docker ps -q 2>/dev/null)
if [ -z "$ids" ]; then
if docker info >/dev/null 2>&1; then printf 'No running containers\n'; else printf 'Docker access unavailable\n'; fi
else
for id in $ids; do
name=$(docker inspect --format '{{.Name}}' "$id" 2>/dev/null | sed 's,^/,,')
version=$(docker exec "$id" php -v 2>/dev/null | head -1)
if [ -n "$version" ]; then printf '%s %s\n' "$name" "$version"; fi
done
fi
if ! command -v docker >/dev/null 2>&1; then
printf 'STATUS|not-installed\n'
elif ! docker info >/dev/null 2>&1; then
printf 'STATUS|permission-denied\n'
else
printf 'Docker not installed\n'
ids=$(docker ps -q 2>/dev/null)
if [ -z "$ids" ]; then printf 'STATUS|no-running-containers\n'; fi
for id in $ids; do
name=$(docker inspect --format '{{.Name}}' "$id" 2>/dev/null | sed 's,^/,,')
image=$(docker inspect --format '{{.Config.Image}}' "$id" 2>/dev/null)
version=$(docker exec "$id" php -r 'echo PHP_VERSION;' 2>/dev/null || true)
if [ -n "$version" ]; then
printf 'PHP|%s|%s|%s\n' "$name" "$image" "$version"
else
printf 'NO_PHP_CLI|%s|%s\n' "$name" "$image"
fi
done
fi
printf '===RECENT_PHP_FILES===\n'
if [ -d /var/www ]; then
@@ -132,11 +136,26 @@ enum SSHScanner {
result.webErrorSignals = (fields["WEB_ERROR_SIGNALS"] ?? []).filter { !$0.hasPrefix("Nginx error log unavailable") }
result.investigationStatus = (fields["WEB_ERROR_SIGNALS"] ?? []).contains(where: { $0.hasPrefix("Nginx error log unavailable") })
? "Partial: Nginx error log inaccessible or absent" : "Read-only indicators collected (not proof of compromise)"
result.dockerPHP = (fields["DOCKER"] ?? []).filter { !$0.hasPrefix("Docker ") && $0 != "No running containers" }
let dockerLines = fields["DOCKER"] ?? []
result.dockerPHP = dockerLines.filter { $0.hasPrefix("PHP|") }.map { line in
let fields = line.split(separator: "|", omittingEmptySubsequences: false)
return fields.count == 4 ? "\(fields[1]) [\(fields[2])] PHP \(fields[3])" : line
}
let noPHP = dockerLines.filter { $0.hasPrefix("NO_PHP_CLI|") }
let dockerStatus = dockerLines.first(where: { $0.hasPrefix("STATUS|") })
if let dockerStatus {
switch dockerStatus {
case "STATUS|permission-denied": result.dockerAuditStatus = "Docker inaccessible: permission or daemon error"
case "STATUS|not-installed": result.dockerAuditStatus = "Docker not installed"
case "STATUS|no-running-containers": result.dockerAuditStatus = "No running containers"
default: result.dockerAuditStatus = "Docker inspection incomplete"
}
} else {
result.dockerAuditStatus = "\(result.dockerPHP.count) PHP runtimes; \(noPHP.count) containers without PHP CLI (not a security finding)"
}
result.packageAuditStatus = result.packageOrigins.contains("APT metadata unavailable")
? "APT package provenance unavailable"
: "Installed and candidate package versions collected; vendor CVE fix status requires advisory verification"
result.dockerAuditStatus = fields["DOCKER"]?.first(where: { $0.hasPrefix("Docker ") || $0 == "No running containers" }) ?? "Running containers inspected where accessible"
if result.phpVersions.isEmpty {
result.findings.append(Finding(severity: "Info", title: "No PHP CLI detected", detail: "No PHP binary found in standard paths; this does not exclude bundled runtimes or containers."))
}
@@ -147,6 +166,19 @@ enum SSHScanner {
result.findings.append(Finding(severity: "Warning", title: "Legacy PHP detected", detail: "\(line). Verify vendor support and distribution security maintenance."))
}
}
for line in result.packageOrigins where line.contains("installed=") && line.contains("candidate=") {
let parts = line.split(separator: " ")
guard let name = parts.first, name.hasPrefix("php") else { continue }
let installed = parts.first(where: { $0.hasPrefix("installed=") }).map { String($0.dropFirst(10)) } ?? ""
let candidate = parts.first(where: { $0.hasPrefix("candidate=") }).map { String($0.dropFirst(10)) } ?? ""
if !installed.isEmpty && installed == candidate {
// Equal revisions indicate no available APT upgrade, not confirmed CVE remediation.
continue
}
if !installed.isEmpty && !candidate.isEmpty && candidate != "(none)" {
result.findings.append(Finding(severity: "Info", title: "Package revision review", detail: "\(name): installed \(installed), candidate \(candidate). Confirm with vendor advisory and dpkg version ordering."))
}
}
for line in result.availableUpdates where line.hasPrefix("php") && line.contains("/") {
result.findings.append(Finding(severity: "Warning", title: "PHP package update available", detail: line + ". Review the package changelog and vendor security notice before classifying CVE exposure."))
}
@@ -154,7 +186,10 @@ enum SSHScanner {
result.findings.append(Finding(severity: "Warning", title: "Package provenance unavailable", detail: "Could not inspect APT package origins; vendor security fixes cannot be verified."))
}
for line in result.dockerPHP {
result.findings.append(Finding(severity: "Info", title: "PHP runtime in Docker", detail: line + ". Container image security status requires a separate image/advisory assessment."))
result.findings.append(Finding(severity: "Info", title: "Container PHP runtime detected", detail: line + ". Verify the container image and upstream PHP security release independently."))
}
if result.dockerAuditStatus.hasPrefix("Docker inaccessible") {
result.findings.append(Finding(severity: "Warning", title: "Docker coverage unavailable", detail: result.dockerAuditStatus))
}
if !result.webErrorSignals.isEmpty {
result.findings.append(Finding(severity: "Warning", title: "Web server error signals", detail: "\(result.webErrorSignals.count) potentially relevant log entries. Review context; errors alone do not establish an attack."))
+3
View File
@@ -36,3 +36,6 @@ The SSH audit collects at most 40 recently modified PHP paths from `/var/www` an
## Branding
The app icon is compiled from `PHPGuard/Assets.xcassets/AppIcon.appiconset`. The DMG includes a branded image at `.background/PHPGuard-Installer.png`; this is an included asset, not a custom Finder DMG window background layout.
## Phase 1.5
Import JSON scans using the dashboard button. Re-scan servers to collect structured Docker evidence. A missing PHP binary inside a container is not a vulnerability. Package version comparisons do not verify vendor CVE fixes.
+9 -8
View File
@@ -1,10 +1,11 @@
# PHPGuard 1.4.0 (Build 5)
# PHPGuard 1.5.0 — Scan accuracy and report import
- Added a native macOS application icon and branded installer graphic asset.
- Added read-only security investigation: recently modified PHP files and selected Nginx error signals.
- Added investigation coverage reporting and non-conclusive findings.
- Preserved existing server inventory, Docker inspection, CVE research and scan history.
- Security signals are not proof of compromise; vendor CVE fix matching remains pending.
- Docker detection now distinguishes a PHP runtime from a container without a PHP CLI. Missing PHP is no longer reported as a PHP finding.
- Docker daemon access failures are reported as incomplete coverage.
- Container image identifiers and PHP versions are collected for identified PHP runtimes.
- Older scan history is normalized to suppress erroneous Docker findings while preserving the raw scan output.
- Import existing JSON scan reports, including Pegasus scans, with duplicate detection.
- Package installed/candidate revision differences are surfaced for review; no CVE exposure is inferred without vendor evidence.
- Read-only SSH collection; no remote changes or updates.
Build: `./Scripts/build.sh`
Release: `NOTARY_PROFILE=orion-notary ./Scripts/release.sh`
Known limitation: CVE-to-Ubuntu-USN package revision verification is not yet implemented. NVD keyword results remain research only. macOS build and notarization must be run on a Mac.