diff --git a/PHPGuard.xcodeproj/project.xcworkspace/xcuserdata/markgregory.xcuserdatad/UserInterfaceState.xcuserstate b/PHPGuard.xcodeproj/project.xcworkspace/xcuserdata/markgregory.xcuserdatad/UserInterfaceState.xcuserstate index f20686e..bdfbdce 100644 Binary files a/PHPGuard.xcodeproj/project.xcworkspace/xcuserdata/markgregory.xcuserdatad/UserInterfaceState.xcuserstate and b/PHPGuard.xcodeproj/project.xcworkspace/xcuserdata/markgregory.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/PHPGuard/AuditStore.swift b/PHPGuard/AuditStore.swift index b79ae38..ca65c86 100644 --- a/PHPGuard/AuditStore.swift +++ b/PHPGuard/AuditStore.swift @@ -6,6 +6,10 @@ import SwiftUI @Published var results: [AuditResult] = [] { didSet { saveResults() } } @Published var scanning: Set = [] @Published var lastError: String? + @Published var discoveredCVEs: [CVERecord] = [] + @Published var cveLastChecked: Date? + @Published var cveError: String? + @Published var fetchingCVEs = false private var folder: URL { let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] @@ -43,6 +47,18 @@ import SwiftUI } } func scanAll() { servers.forEach { scan($0) } } + func refreshCVEs() { + guard !fetchingCVEs else { return } + fetchingCVEs = true + cveError = nil + Task { + defer { fetchingCVEs = false } + do { + discoveredCVEs = try await VulnerabilityService.discover() + cveLastChecked = Date() + } catch { cveError = error.localizedDescription } + } + } func export(_ result: AuditResult) { let panel = NSSavePanel() panel.allowedContentTypes = [.json] diff --git a/PHPGuard/ContentView.swift b/PHPGuard/ContentView.swift index 1ff8f65..ec63349 100644 --- a/PHPGuard/ContentView.swift +++ b/PHPGuard/ContentView.swift @@ -31,6 +31,7 @@ struct ContentView: View { } } Section("Navigation") { + Label("CVE research", systemImage: "shield.lefthalf.filled").tag(UUID(uuidString: "00000000-0000-0000-0000-000000000002")!) Label("Scan history", systemImage: "clock.arrow.circlepath").tag(UUID(uuidString: "00000000-0000-0000-0000-000000000001")!) } } @@ -39,7 +40,9 @@ struct ContentView: View { Button { editing = nil; showingEditor = true } label: { Image(systemName: "plus") }.help("Add server") } } detail: { - if selection == UUID(uuidString: "00000000-0000-0000-0000-000000000001") { + if selection == UUID(uuidString: "00000000-0000-0000-0000-000000000002") { + cveView + } else if selection == UUID(uuidString: "00000000-0000-0000-0000-000000000001") { historyView } else if let server = store.servers.first(where: { $0.id == selection }) { serverView(server) @@ -125,6 +128,32 @@ struct ContentView: View { else { ForEach(values, id: \.self) { Text($0).font(.system(.body, design: .monospaced)).textSelection(.enabled) } } } } + private var cveView: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("PHP CVE research").font(.largeTitle.bold()) + Spacer() + if store.fetchingCVEs { ProgressView() } + Button("Refresh from NVD") { store.refreshCVEs() }.disabled(store.fetchingCVEs) + } + Text("Research candidates from the NVD keyword index. These records are NOT matched to installed PHP versions, Ubuntu backports or Docker images. Do not interpret them as confirmed server vulnerabilities.") + .foregroundStyle(.secondary) + if let date = store.cveLastChecked { Text("Last fetched: \(date.formatted())").foregroundStyle(.secondary) } + if let error = store.cveError { Text(error).foregroundStyle(.red) } + List(store.discoveredCVEs) { cve in + VStack(alignment: .leading, spacing: 6) { + HStack { + Link(cve.id, destination: URL(string: cve.referenceURL)!) + .font(.headline) + Spacer() + Text(cve.severity + (cve.score.map { " (\($0))" } ?? "")) + Text(String(cve.published.prefix(10))).foregroundStyle(.secondary) + } + Text(cve.description).lineLimit(4).textSelection(.enabled) + }.padding(.vertical, 5) + } + }.padding(24) + } private var historyView: some View { List(store.results) { result in HStack { diff --git a/PHPGuard/Models.swift b/PHPGuard/Models.swift index 54b52f4..9220516 100644 --- a/PHPGuard/Models.swift +++ b/PHPGuard/Models.swift @@ -35,3 +35,13 @@ struct AuditResult: Codable, Identifiable { var rawOutput = "" var error: String? = nil } + + +struct CVERecord: Codable, Identifiable { + var id: String + var description: String + var published: String + var severity: String + var score: Double? + var referenceURL: String { "https://nvd.nist.gov/vuln/detail/" + id } +} diff --git a/README.md b/README.md index 310d403..2a73a79 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,31 @@ -# PHPGuard — Phase 2 +# PHPGuard 1.2 — Phase 3 -Native SwiftUI macOS application for **read-only** SSH inventory of PHP installations on Linux servers. +Native macOS SwiftUI PHP inventory and CVE research tool. -## New in Phase 2 -- APT PHP package upgrade inventory (no automatic updates) -- PHP version discovery inside accessible running Docker containers -- Per-server findings and status of package/Docker checks -- Existing SSH inventory, scan history, and JSON export retained +## Included +- Phase 2 SSH inventory, APT update listing, Docker PHP inventory, JSON reports. +- Phase 3 **CVE research** screen using the public NVD 2.0 API, with published dates, descriptions, and CVSS metadata. +- NVD records are keyword-search **candidates only**, NOT confirmed vulnerabilities on your servers. +- No Ubuntu package revision/backport matching is performed; no server is marked secure. +- `Scripts/build.sh` for unsigned local Debug builds and optional launch. +- `Scripts/release.sh` for Release build, Developer ID signing, DMG, notarization and stapling. -## Security interpretation -**This is not a CVE scanner yet.** Available upgrades are not necessarily security updates; the absence of upgrades is not evidence of safety. Vendor backports and security advisories must be checked before labelling an installation vulnerable or patched. Docker inspection only checks running containers with a `php` executable accessible on PATH. No compromise detection is performed. +## Build +```bash +./Scripts/build.sh +LAUNCH_AFTER_BUILD=0 CONFIGURATION=Release ./Scripts/build.sh +``` -## Open in Xcode -Unzip the archive, open `PHPGuard/PHPGuard.xcodeproj`, select My Mac, and build. macOS 15+; the build has not been verified in Xcode in this environment. +## Release +Configure a notarytool keychain profile on your Mac, then run: +```bash +NOTARY_PROFILE=your-profile ./Scripts/release.sh +``` +The script auto-detects a Developer ID Application identity, or use `SIGNING_IDENTITY`. +Release artifacts go to `dist/`. No automatic upload or publication. -## SSH -The app invokes `/usr/bin/ssh` with BatchMode and StrictHostKeyChecking. Configure working key-based SSH access in Terminal first. Remote scripts are read-only; APT's upgrade listing may refresh no metadata and uses currently cached package lists. Docker access depends on remote user permissions. +## Security +SSH requires existing known_hosts entries and key-based authentication. Scans are read-only. NVD results are for research, not a substitute for vendor-specific package advisories. + +## Known limitations +Not built/tested with Xcode in this environment. CVE discovery is broad keyword search and may include unrelated products or omit relevant vulnerabilities. NVD may rate-limit requests. Automatic backport assessment is a future phase. diff --git a/Scripts/build.sh b/Scripts/build.sh new file mode 100755 index 0000000..93d3a30 --- /dev/null +++ b/Scripts/build.sh @@ -0,0 +1,11 @@ +#!/bin/zsh +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CONFIG="${CONFIGURATION:-Debug}" +DERIVED="${DERIVED_DATA_PATH:-$ROOT/.build}" +command -v xcodebuild >/dev/null || { echo "Xcode command line tools required" >&2; exit 1; } +xcodebuild -project "$ROOT/PHPGuard.xcodeproj" -scheme PHPGuard -configuration "$CONFIG" -derivedDataPath "$DERIVED" CODE_SIGNING_ALLOWED=NO build +APP="$DERIVED/Build/Products/$CONFIG/PHPGuard.app" +[[ -d "$APP" ]] || { echo "Built application not found: $APP" >&2; exit 1; } +echo "Built: $APP" +if [[ "${LAUNCH_AFTER_BUILD:-1}" == "1" ]]; then open "$APP"; fi diff --git a/Scripts/release.sh b/Scripts/release.sh new file mode 100755 index 0000000..59d966a --- /dev/null +++ b/Scripts/release.sh @@ -0,0 +1,31 @@ +#!/bin/zsh +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +command -v xcodebuild >/dev/null || { echo "Xcode is required" >&2; exit 1; } +command -v hdiutil >/dev/null || { echo "hdiutil missing" >&2; exit 1; } +IDENTITY="${SIGNING_IDENTITY:-$(security find-identity -v -p codesigning | sed -nE 's/.*"(Developer ID Application: [^"]+)".*/\1/p' | head -1)}" +[[ -n "$IDENTITY" ]] || { echo "Developer ID Application signing identity required. Set SIGNING_IDENTITY." >&2; exit 1; } +PROFILE="${NOTARY_PROFILE:-}" +[[ -n "$PROFILE" ]] || { echo "Set NOTARY_PROFILE to an existing notarytool keychain profile." >&2; exit 1; } +VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$ROOT/.build/Build/Products/Release/PHPGuard.app/Contents/Info.plist" 2>/dev/null || true)" +DERIVED="$ROOT/.build" +xcodebuild -project "$ROOT/PHPGuard.xcodeproj" -scheme PHPGuard -configuration Release -derivedDataPath "$DERIVED" CODE_SIGNING_ALLOWED=NO build +APP="$DERIVED/Build/Products/Release/PHPGuard.app" +[[ -d "$APP" ]] || { echo "Missing Release app" >&2; exit 1; } +VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist")" +mkdir -p "$ROOT/dist" +STAGING="$(mktemp -d)" +trap 'rm -rf "$STAGING"' EXIT +cp -R "$APP" "$STAGING/PHPGuard.app" +# No --deep signing: sign embedded executables individually if added in future. +codesign --force --options runtime --timestamp --sign "$IDENTITY" "$STAGING/PHPGuard.app" +codesign --verify --strict --verbose=2 "$STAGING/PHPGuard.app" +DMG="$ROOT/dist/PHPGuard-$VERSION.dmg" +rm -f "$DMG" +ln -s /Applications "$STAGING/Applications" +hdiutil create -volname "PHPGuard $VERSION" -srcfolder "$STAGING" -ov -format UDZO "$DMG" +xcrun notarytool submit "$DMG" --keychain-profile "$PROFILE" --wait +xcrun stapler staple "$DMG" +xcrun stapler validate "$DMG" +echo "Release ready: $DMG"