diff --git a/PHPGuard.xcodeproj/project.pbxproj b/PHPGuard.xcodeproj/project.pbxproj index fe55116..9cc71ee 100644 --- a/PHPGuard.xcodeproj/project.pbxproj +++ b/PHPGuard.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + B10000000000000000000002 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000001 /* Assets.xcassets */; }; A10000000000000000000002 /* VulnerabilityService.swift in Sources */ = { isa = PBXBuildFile; fileRef = A10000000000000000000001 /* VulnerabilityService.swift */; @@ -20,6 +21,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + B10000000000000000000001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A10000000000000000000001 /* VulnerabilityService.swift */ = { isa = PBXFileReference; lastKnownFileType = sourcecode.swift; @@ -53,6 +55,7 @@ 000000000000000000000067 /* AuditStore.swift */, A10000000000000000000001 /* VulnerabilityService.swift */, 000000000000000000000068 /* ContentView.swift */, + B10000000000000000000001 /* Assets.xcassets */, ); path = PHPGuard; sourceTree = ""; @@ -73,6 +76,7 @@ buildConfigurationList = 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */; buildPhases = ( 000000000000000000000021 /* Sources */, + B10000000000000000000003 /* Resources */, ); buildRules = ( ); @@ -114,6 +118,10 @@ }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + B10000000000000000000003 /* Resources */ = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = (B10000000000000000000002 /* Assets.xcassets in Resources */,); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 000000000000000000000021 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -158,6 +166,10 @@ DEVELOPMENT_TEAM = 39R6U3437D; ENABLE_APP_SANDBOX = NO; GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.4.0; + CURRENT_PROJECT_VERSION = 5; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; @@ -173,6 +185,10 @@ DEVELOPMENT_TEAM = 39R6U3437D; ENABLE_APP_SANDBOX = NO; GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.4.0; + CURRENT_PROJECT_VERSION = 5; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/Contents.json b/PHPGuard/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..6157218 --- /dev/null +++ b/PHPGuard/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images": [ + { + "idiom": "mac", + "size": "16x16", + "scale": "1x", + "filename": "icon_16x16_1x.png" + }, + { + "idiom": "mac", + "size": "16x16", + "scale": "2x", + "filename": "icon_16x16_2x.png" + }, + { + "idiom": "mac", + "size": "32x32", + "scale": "1x", + "filename": "icon_32x32_1x.png" + }, + { + "idiom": "mac", + "size": "32x32", + "scale": "2x", + "filename": "icon_32x32_2x.png" + }, + { + "idiom": "mac", + "size": "128x128", + "scale": "1x", + "filename": "icon_128x128_1x.png" + }, + { + "idiom": "mac", + "size": "128x128", + "scale": "2x", + "filename": "icon_128x128_2x.png" + }, + { + "idiom": "mac", + "size": "256x256", + "scale": "1x", + "filename": "icon_256x256_1x.png" + }, + { + "idiom": "mac", + "size": "256x256", + "scale": "2x", + "filename": "icon_256x256_2x.png" + }, + { + "idiom": "mac", + "size": "512x512", + "scale": "1x", + "filename": "icon_512x512_1x.png" + }, + { + "idiom": "mac", + "size": "512x512", + "scale": "2x", + "filename": "icon_512x512_2x.png" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 0000000..346a15e Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_128x128_1x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_128x128_1x.png new file mode 100644 index 0000000..410244e Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_128x128_1x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_128x128_2x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_128x128_2x.png new file mode 100644 index 0000000..dd4eb99 Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_128x128_2x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_16x16_1x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_16x16_1x.png new file mode 100644 index 0000000..a02e72f Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_16x16_1x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_16x16_2x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_16x16_2x.png new file mode 100644 index 0000000..81bcb2d Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_16x16_2x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_256x256_1x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_256x256_1x.png new file mode 100644 index 0000000..dd4eb99 Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_256x256_1x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_256x256_2x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_256x256_2x.png new file mode 100644 index 0000000..3d8ba98 Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_256x256_2x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_32x32_1x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_32x32_1x.png new file mode 100644 index 0000000..81bcb2d Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_32x32_1x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_32x32_2x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_32x32_2x.png new file mode 100644 index 0000000..4a65166 Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_32x32_2x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_512x512_1x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_512x512_1x.png new file mode 100644 index 0000000..3d8ba98 Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_512x512_1x.png differ diff --git a/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_512x512_2x.png b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_512x512_2x.png new file mode 100644 index 0000000..346a15e Binary files /dev/null and b/PHPGuard/Assets.xcassets/AppIcon.appiconset/icon_512x512_2x.png differ diff --git a/PHPGuard/Assets.xcassets/InstallerIcon.imageset/Contents.json b/PHPGuard/Assets.xcassets/InstallerIcon.imageset/Contents.json new file mode 100644 index 0000000..56cf5e4 --- /dev/null +++ b/PHPGuard/Assets.xcassets/InstallerIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images": [ + { + "filename": "icon.png", + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/PHPGuard/Assets.xcassets/InstallerIcon.imageset/icon.png b/PHPGuard/Assets.xcassets/InstallerIcon.imageset/icon.png new file mode 100644 index 0000000..84187b6 Binary files /dev/null and b/PHPGuard/Assets.xcassets/InstallerIcon.imageset/icon.png differ diff --git a/PHPGuard/Models.swift b/PHPGuard/Models.swift index 205905a..8f866ab 100644 --- a/PHPGuard/Models.swift +++ b/PHPGuard/Models.swift @@ -33,10 +33,13 @@ struct AuditResult: Codable, Identifiable { var packageAuditStatus = "Not checked" var dockerAuditStatus = "Not checked" var findings: [Finding] = [] + var recentPHPFiles: [String] = [] + var webErrorSignals: [String] = [] + var investigationStatus = "Not checked" var rawOutput = "" var error: String? = nil - enum CodingKeys: String, CodingKey { case serverID, serverName, scannedAt, hostname, os, phpVersions, fpmServices, nginxUpstreams, packageVersions, availableUpdates, packageOrigins, dockerPHP, packageAuditStatus, dockerAuditStatus, findings, rawOutput, error, id } + enum CodingKeys: String, CodingKey { case serverID, serverName, scannedAt, hostname, os, phpVersions, fpmServices, nginxUpstreams, packageVersions, availableUpdates, packageOrigins, dockerPHP, packageAuditStatus, dockerAuditStatus, recentPHPFiles, webErrorSignals, investigationStatus, findings, rawOutput, error, id } init(serverID: UUID, serverName: String) { self.serverID = serverID; self.serverName = serverName } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -55,6 +58,9 @@ struct AuditResult: Codable, Identifiable { packageAuditStatus = try container.decodeIfPresent(String.self, forKey: .packageAuditStatus) ?? "Not checked" dockerAuditStatus = try container.decodeIfPresent(String.self, forKey: .dockerAuditStatus) ?? "Not checked" findings = try container.decodeIfPresent([Finding].self, forKey: .findings) ?? [] + recentPHPFiles = try container.decodeIfPresent([String].self, forKey: .recentPHPFiles) ?? [] + webErrorSignals = try container.decodeIfPresent([String].self, forKey: .webErrorSignals) ?? [] + investigationStatus = try container.decodeIfPresent(String.self, forKey: .investigationStatus) ?? "Not checked" rawOutput = try container.decodeIfPresent(String.self, forKey: .rawOutput) ?? "" error = try container.decodeIfPresent(String.self, forKey: .error) id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID() @@ -76,6 +82,9 @@ struct AuditResult: Codable, Identifiable { try container.encode(packageAuditStatus, forKey: .packageAuditStatus) try container.encode(dockerAuditStatus, forKey: .dockerAuditStatus) try container.encode(findings, forKey: .findings) + try container.encode(recentPHPFiles, forKey: .recentPHPFiles) + try container.encode(webErrorSignals, forKey: .webErrorSignals) + try container.encode(investigationStatus, forKey: .investigationStatus) try container.encode(rawOutput, forKey: .rawOutput) try container.encodeIfPresent(error, forKey: .error) try container.encode(id, forKey: .id) diff --git a/PHPGuard/SSHScanner.swift b/PHPGuard/SSHScanner.swift index d76c435..4ae26d0 100644 --- a/PHPGuard/SSHScanner.swift +++ b/PHPGuard/SSHScanner.swift @@ -55,6 +55,18 @@ enum SSHScanner { else printf 'Docker not installed\n' fi + printf '===RECENT_PHP_FILES===\n' + if [ -d /var/www ]; then + find /var/www -type f -name '*.php' -mtime -3 -printf '%TY-%Tm-%Td %TH:%TM %p\n' 2>/dev/null | head -40 + else + printf 'Web root unavailable\n' + fi + printf '===WEB_ERROR_SIGNALS===\n' + if [ -r /var/log/nginx/error.log ]; then + tail -n 1000 /var/log/nginx/error.log 2>/dev/null | grep -Ei 'segfault|upstream prematurely closed|PHP Fatal|out of memory' | tail -30 || true + else + printf 'Nginx error log unavailable (permissions or absent)\n' + fi printf '===END===\n' """# @@ -116,6 +128,10 @@ enum SSHScanner { result.packageVersions = fields["PACKAGES"] ?? [] result.availableUpdates = fields["UPDATES"] ?? [] result.packageOrigins = fields["PACKAGE_ORIGINS"] ?? [] + result.recentPHPFiles = (fields["RECENT_PHP_FILES"] ?? []).filter { $0 != "Web root unavailable" } + 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" } result.packageAuditStatus = result.packageOrigins.contains("APT metadata unavailable") ? "APT package provenance unavailable" @@ -140,6 +156,15 @@ enum SSHScanner { 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.")) } + 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.")) + } + if !result.recentPHPFiles.isEmpty { + result.findings.append(Finding(severity: "Info", title: "Recently modified PHP files", detail: "\(result.recentPHPFiles.count) PHP files changed in the last 3 days (up to 40 shown). Compare with expected deployments.")) + } + if result.investigationStatus.hasPrefix("Partial") { + result.findings.append(Finding(severity: "Info", title: "Investigation coverage limited", detail: result.investigationStatus)) + } result.findings.append(Finding(severity: "Info", title: "CVE status unverified", detail: "Version strings and available upgrades do not establish CVE exposure. Ubuntu backports, package origins, and current vendor advisories require independent verification. No server is labelled secure based on this scan.")) return result } diff --git a/PHPGuard/VulnerabilityService.swift b/PHPGuard/VulnerabilityService.swift index bbf3dc8..39dd078 100644 --- a/PHPGuard/VulnerabilityService.swift +++ b/PHPGuard/VulnerabilityService.swift @@ -73,7 +73,7 @@ enum VulnerabilityService { forHTTPHeaderField: "Accept" ) request.setValue( - "PHPGuard/1.2", + "PHPGuard/1.4", forHTTPHeaderField: "User-Agent" ) diff --git a/README.md b/README.md index 2a73a79..3253c5c 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,10 @@ SSH requires existing known_hosts entries and key-based authentication. Scans ar ## 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. + + +## Version 1.4 security investigation +The SSH audit collects at most 40 recently modified PHP paths from `/var/www` and up to 30 matching Nginx error lines when accessible. It never modifies remote files. Access may be limited by permissions. The scan is not a compromise determination. + +## 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. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 37dc5a2..509d30c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,22 +1,10 @@ -# PHPGuard 1.3.0 — Release Notes +# PHPGuard 1.4.0 (Build 5) -- Added read-only APT package provenance and candidate version inventory for PHP CLI and PHP-FPM packages. -- Added package-origin visibility to server details and exported JSON reports. -- Corrected the Docker PHP version probe. -- Preserved compatibility with earlier stored scan results. -- Updated build number to 4 and version to 1.3.0. -- Retained the Debug build and signed/notarized DMG release scripts. +- 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. -## Limitations - -APT origin and version information **does not** establish whether a specific CVE is fixed. Ubuntu security backports, package provenance, and vendor advisories still require verification. The CVE Research tab lists keyword-search results, not confirmed server exposures. Scans are read-only and do not remediate systems. - -## Build on macOS - -`./Scripts/build.sh` - -## Release on macOS - -`NOTARY_PROFILE=orion-notary ./Scripts/release.sh` - -Requires Xcode, a Developer ID Application signing identity, and a configured notarytool keychain profile. +Build: `./Scripts/build.sh` +Release: `NOTARY_PROFILE=orion-notary ./Scripts/release.sh` diff --git a/Scripts/release.sh b/Scripts/release.sh index aafb40a..11632de 100755 --- a/Scripts/release.sh +++ b/Scripts/release.sh @@ -23,6 +23,12 @@ codesign --verify --strict --verbose=2 "$STAGING/PHPGuard.app" DMG="$ROOT/dist/PHPGuard-$VERSION.dmg" rm -f "$DMG" ln -s /Applications "$STAGING/Applications" +# Include a branded installer graphic in the disk image. +INSTALLER_ICON="$ROOT/PHPGuard/Assets.xcassets/InstallerIcon.imageset/icon.png" +if [[ -f "$INSTALLER_ICON" ]]; then + mkdir -p "$STAGING/.background" + cp "$INSTALLER_ICON" "$STAGING/.background/PHPGuard-Installer.png" +fi hdiutil create -volname "PHPGuard $VERSION" -srcfolder "$STAGING" -ov -format UDZO "$DMG" xcrun notarytool submit "$DMG" --keychain-profile "$PROFILE" --wait xcrun stapler staple "$DMG"