From 0c4a1625b08a95e4d2b75554b962c9543f5d6dc5 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Thu, 22 Oct 2026 12:55:49 +0100 Subject: [PATCH] Updates to scanner --- PHPGuard.xcodeproj/project.pbxproj | 8 ++-- PHPGuard/AuditStore.swift | 32 ++++++++++++++- PHPGuard/ContentView.swift | 5 ++- PHPGuard/SSHScanner.swift | 65 +++++++++++++++++++++++------- README.md | 3 ++ RELEASE_NOTES.md | 17 ++++---- 6 files changed, 101 insertions(+), 29 deletions(-) diff --git a/PHPGuard.xcodeproj/project.pbxproj b/PHPGuard.xcodeproj/project.pbxproj index 9cc71ee..43dd04f 100644 --- a/PHPGuard.xcodeproj/project.pbxproj +++ b/PHPGuard.xcodeproj/project.pbxproj @@ -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; diff --git a/PHPGuard/AuditStore.swift b/PHPGuard/AuditStore.swift index ca65c86..0102ba5 100644 --- a/PHPGuard/AuditStore.swift +++ b/PHPGuard/AuditStore.swift @@ -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(_ name: String) -> T? { guard let data = try? Data(contentsOf: folder.appendingPathComponent(name)) else { return nil } diff --git a/PHPGuard/ContentView.swift b/PHPGuard/ContentView.swift index 82584f6..9722ef0 100644 --- a/PHPGuard/ContentView.swift +++ b/PHPGuard/ContentView.swift @@ -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) } diff --git a/PHPGuard/SSHScanner.swift b/PHPGuard/SSHScanner.swift index 4ae26d0..9472d8d 100644 --- a/PHPGuard/SSHScanner.swift +++ b/PHPGuard/SSHScanner.swift @@ -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.")) diff --git a/README.md b/README.md index 3253c5c..91ab851 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 509d30c..0890394 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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.