Updates to scanner
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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."))
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user