diff --git a/PHPGuard.xcodeproj/project.pbxproj b/PHPGuard.xcodeproj/project.pbxproj index ba0b93a..1fdb41b 100644 --- a/PHPGuard.xcodeproj/project.pbxproj +++ b/PHPGuard.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - B10000000000000000000002 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000001 /* Assets.xcassets */; }; + C10000000000000000000002 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C10000000000000000000001 /* Assets.xcassets */; }; A10000000000000000000002 /* VulnerabilityService.swift in Sources */ = { isa = PBXBuildFile; fileRef = A10000000000000000000001 /* VulnerabilityService.swift */; @@ -18,10 +18,12 @@ 0000000000000000000000CA /* SSHScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000066 /* SSHScanner.swift */; }; 0000000000000000000000CB /* AuditStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000067 /* AuditStore.swift */; }; 0000000000000000000000CC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000068 /* ContentView.swift */; }; + B20000000000000000000001 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000001 /* SettingsView.swift */; }; + B20000000000000000000002 /* ReportPDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000002 /* ReportPDF.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - B10000000000000000000001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + C10000000000000000000001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A10000000000000000000001 /* VulnerabilityService.swift */ = { isa = PBXFileReference; lastKnownFileType = sourcecode.swift; @@ -35,6 +37,8 @@ 000000000000000000000066 /* SSHScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHScanner.swift; sourceTree = ""; }; 000000000000000000000067 /* AuditStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditStore.swift; sourceTree = ""; }; 000000000000000000000068 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B10000000000000000000001 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + B10000000000000000000002 /* ReportPDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportPDF.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXGroup section */ @@ -55,7 +59,8 @@ 000000000000000000000067 /* AuditStore.swift */, A10000000000000000000001 /* VulnerabilityService.swift */, 000000000000000000000068 /* ContentView.swift */, - B10000000000000000000001 /* Assets.xcassets */, + B10000000000000000000002 /* ReportPDF.swift */, + B10000000000000000000001 /* SettingsView.swift */, ); path = PHPGuard; sourceTree = ""; @@ -119,7 +124,9 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - B10000000000000000000003 /* Resources */ = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = (B10000000000000000000002 /* Assets.xcassets in Resources */,); runOnlyForDeploymentPostprocessing = 0; }; + B10000000000000000000003 /* Resources */ = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C10000000000000000000002 /* Assets.xcassets in Resources */, + ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -131,8 +138,10 @@ 0000000000000000000000C9 /* Models.swift in Sources */, 0000000000000000000000CA /* SSHScanner.swift in Sources */, 0000000000000000000000CB /* AuditStore.swift in Sources */, - A10000000000000000000002 /* VulnerabilityService.swift in Sources */, 0000000000000000000000CC /* ContentView.swift in Sources */, + A10000000000000000000002 /* VulnerabilityService.swift in Sources */, + B20000000000000000000002 /* ReportPDF.swift in Sources */, + B20000000000000000000001 /* SettingsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -168,8 +177,8 @@ GENERATE_INFOPLIST_FILE = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; - MARKETING_VERSION = 1.8.0; - CURRENT_PROJECT_VERSION = 9; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; @@ -187,8 +196,8 @@ GENERATE_INFOPLIST_FILE = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; - MARKETING_VERSION = 1.8.0; - CURRENT_PROJECT_VERSION = 9; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; diff --git a/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131245 b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131245 new file mode 100644 index 0000000..bf8a926 --- /dev/null +++ b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131245 @@ -0,0 +1,232 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + 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 */; + }; + + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000064 /* PHPGuardApp.swift */; }; + 0000000000000000000000C9 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000065 /* Models.swift */; }; + 0000000000000000000000CA /* SSHScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000066 /* SSHScanner.swift */; }; + 0000000000000000000000CB /* AuditStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000067 /* AuditStore.swift */; }; + 0000000000000000000000CC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000068 /* ContentView.swift */; }; + B20000000000000000000001 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000001 /* SettingsView.swift */; }; + B20000000000000000000002 /* ReportPDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000002 /* ReportPDF.swift */; }; +/* 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; + path = VulnerabilityService.swift; + sourceTree = ""; + }; + + 000000000000000000000030 /* PHPGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PHPGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 000000000000000000000064 /* PHPGuardApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPGuardApp.swift; sourceTree = ""; }; + 000000000000000000000065 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 000000000000000000000066 /* SSHScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHScanner.swift; sourceTree = ""; }; + 000000000000000000000067 /* AuditStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditStore.swift; sourceTree = ""; }; + 000000000000000000000068 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B10000000000000000000001 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + B10000000000000000000002 /* ReportPDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportPDF.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 000000000000000000000010 = { + isa = PBXGroup; + children = ( + 000000000000000000000011 /* PHPGuard */, + 000000000000000000000012 /* Products */, + ); + sourceTree = ""; + }; + 000000000000000000000011 /* PHPGuard */ = { + isa = PBXGroup; + children = ( + 000000000000000000000064 /* PHPGuardApp.swift */, + 000000000000000000000065 /* Models.swift */, + 000000000000000000000066 /* SSHScanner.swift */, + 000000000000000000000067 /* AuditStore.swift */, + A10000000000000000000001 /* VulnerabilityService.swift */, + 000000000000000000000068 /* ContentView.swift */, + B10000000000000000000002 /* ReportPDF.swift */, + B10000000000000000000001 /* SettingsView.swift */, + B10000000000000000000001 /* Assets.xcassets */, + ); + path = PHPGuard; + sourceTree = ""; + }; + 000000000000000000000012 /* Products */ = { + isa = PBXGroup; + children = ( + 000000000000000000000030 /* PHPGuard.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 000000000000000000000020 /* PHPGuard */ = { + isa = PBXNativeTarget; + buildConfigurationList = 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */; + buildPhases = ( + 000000000000000000000021 /* Sources */, + B10000000000000000000003 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PHPGuard; + productName = PHPGuard; + productReference = 000000000000000000000030 /* PHPGuard.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 000000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1600; + TargetAttributes = { + 000000000000000000000020 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 000000000000000000000010; + productRefGroup = 000000000000000000000012 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 000000000000000000000020 /* PHPGuard */, + ); + }; +/* 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; + buildActionMask = 2147483647; + files = ( + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */, + 0000000000000000000000C9 /* Models.swift in Sources */, + 0000000000000000000000CA /* SSHScanner.swift in Sources */, + 0000000000000000000000CB /* AuditStore.swift in Sources */, + A10000000000000000000002 /* VulnerabilityService.swift in Sources */, + 0000000000000000000000CC /* ContentView.swift in Sources */, + B20000000000000000000002 /* ReportPDF.swift in Sources */, + B20000000000000000000001 /* SettingsView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 000000000000000000000051 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000052 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 000000000000000000000053 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000054 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000051 /* Debug */, + 000000000000000000000052 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000053 /* Debug */, + 000000000000000000000054 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 000000000000000000000001 /* Project object */; +} diff --git a/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131305 b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131305 new file mode 100644 index 0000000..fc541f1 --- /dev/null +++ b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131305 @@ -0,0 +1,232 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + 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 */; + }; + + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000064 /* PHPGuardApp.swift */; }; + 0000000000000000000000C9 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000065 /* Models.swift */; }; + 0000000000000000000000CA /* SSHScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000066 /* SSHScanner.swift */; }; + 0000000000000000000000CB /* AuditStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000067 /* AuditStore.swift */; }; + 0000000000000000000000CC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000068 /* ContentView.swift */; }; + B20000000000000000000001 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000001 /* SettingsView.swift */; }; + B20000000000000000000002 /* ReportPDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000002 /* ReportPDF.swift */; }; +/* 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; + path = VulnerabilityService.swift; + sourceTree = ""; + }; + + 000000000000000000000030 /* PHPGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PHPGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 000000000000000000000064 /* PHPGuardApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPGuardApp.swift; sourceTree = ""; }; + 000000000000000000000065 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 000000000000000000000066 /* SSHScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHScanner.swift; sourceTree = ""; }; + 000000000000000000000067 /* AuditStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditStore.swift; sourceTree = ""; }; + 000000000000000000000068 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B10000000000000000000001 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + B10000000000000000000002 /* ReportPDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportPDF.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 000000000000000000000010 = { + isa = PBXGroup; + children = ( + 000000000000000000000011 /* PHPGuard */, + 000000000000000000000012 /* Products */, + ); + sourceTree = ""; + }; + 000000000000000000000011 /* PHPGuard */ = { + isa = PBXGroup; + children = ( + 000000000000000000000064 /* PHPGuardApp.swift */, + 000000000000000000000065 /* Models.swift */, + 000000000000000000000066 /* SSHScanner.swift */, + 000000000000000000000067 /* AuditStore.swift */, + A10000000000000000000001 /* VulnerabilityService.swift */, + 000000000000000000000068 /* ContentView.swift */, + B10000000000000000000002 /* ReportPDF.swift */, + B10000000000000000000001 /* SettingsView.swift */, + ); + path = PHPGuard; + sourceTree = ""; + }; + 000000000000000000000012 /* Products */ = { + isa = PBXGroup; + children = ( + 000000000000000000000030 /* PHPGuard.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 000000000000000000000020 /* PHPGuard */ = { + isa = PBXNativeTarget; + buildConfigurationList = 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */; + buildPhases = ( + 000000000000000000000021 /* Sources */, + B10000000000000000000003 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PHPGuard; + productName = PHPGuard; + productReference = 000000000000000000000030 /* PHPGuard.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 000000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1600; + TargetAttributes = { + 000000000000000000000020 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 000000000000000000000010; + productRefGroup = 000000000000000000000012 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 000000000000000000000020 /* PHPGuard */, + ); + }; +/* 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; + buildActionMask = 2147483647; + files = ( + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */, + 0000000000000000000000C9 /* Models.swift in Sources */, + 0000000000000000000000CA /* SSHScanner.swift in Sources */, + 0000000000000000000000CB /* AuditStore.swift in Sources */, + 0000000000000000000000CC /* ContentView.swift in Sources */, + B20000000000000000000002 /* ReportPDF.swift in Sources */, + B20000000000000000000001 /* SettingsView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 000000000000000000000051 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000052 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 000000000000000000000053 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000054 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000051 /* Debug */, + 000000000000000000000052 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000053 /* Debug */, + 000000000000000000000054 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 000000000000000000000001 /* Project object */; +} diff --git a/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131332 b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131332 new file mode 100644 index 0000000..fc541f1 --- /dev/null +++ b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131332 @@ -0,0 +1,232 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + 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 */; + }; + + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000064 /* PHPGuardApp.swift */; }; + 0000000000000000000000C9 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000065 /* Models.swift */; }; + 0000000000000000000000CA /* SSHScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000066 /* SSHScanner.swift */; }; + 0000000000000000000000CB /* AuditStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000067 /* AuditStore.swift */; }; + 0000000000000000000000CC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000068 /* ContentView.swift */; }; + B20000000000000000000001 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000001 /* SettingsView.swift */; }; + B20000000000000000000002 /* ReportPDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000002 /* ReportPDF.swift */; }; +/* 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; + path = VulnerabilityService.swift; + sourceTree = ""; + }; + + 000000000000000000000030 /* PHPGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PHPGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 000000000000000000000064 /* PHPGuardApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPGuardApp.swift; sourceTree = ""; }; + 000000000000000000000065 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 000000000000000000000066 /* SSHScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHScanner.swift; sourceTree = ""; }; + 000000000000000000000067 /* AuditStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditStore.swift; sourceTree = ""; }; + 000000000000000000000068 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B10000000000000000000001 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + B10000000000000000000002 /* ReportPDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportPDF.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 000000000000000000000010 = { + isa = PBXGroup; + children = ( + 000000000000000000000011 /* PHPGuard */, + 000000000000000000000012 /* Products */, + ); + sourceTree = ""; + }; + 000000000000000000000011 /* PHPGuard */ = { + isa = PBXGroup; + children = ( + 000000000000000000000064 /* PHPGuardApp.swift */, + 000000000000000000000065 /* Models.swift */, + 000000000000000000000066 /* SSHScanner.swift */, + 000000000000000000000067 /* AuditStore.swift */, + A10000000000000000000001 /* VulnerabilityService.swift */, + 000000000000000000000068 /* ContentView.swift */, + B10000000000000000000002 /* ReportPDF.swift */, + B10000000000000000000001 /* SettingsView.swift */, + ); + path = PHPGuard; + sourceTree = ""; + }; + 000000000000000000000012 /* Products */ = { + isa = PBXGroup; + children = ( + 000000000000000000000030 /* PHPGuard.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 000000000000000000000020 /* PHPGuard */ = { + isa = PBXNativeTarget; + buildConfigurationList = 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */; + buildPhases = ( + 000000000000000000000021 /* Sources */, + B10000000000000000000003 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PHPGuard; + productName = PHPGuard; + productReference = 000000000000000000000030 /* PHPGuard.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 000000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1600; + TargetAttributes = { + 000000000000000000000020 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 000000000000000000000010; + productRefGroup = 000000000000000000000012 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 000000000000000000000020 /* PHPGuard */, + ); + }; +/* 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; + buildActionMask = 2147483647; + files = ( + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */, + 0000000000000000000000C9 /* Models.swift in Sources */, + 0000000000000000000000CA /* SSHScanner.swift in Sources */, + 0000000000000000000000CB /* AuditStore.swift in Sources */, + 0000000000000000000000CC /* ContentView.swift in Sources */, + B20000000000000000000002 /* ReportPDF.swift in Sources */, + B20000000000000000000001 /* SettingsView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 000000000000000000000051 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000052 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 000000000000000000000053 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000054 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000051 /* Debug */, + 000000000000000000000052 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000053 /* Debug */, + 000000000000000000000054 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 000000000000000000000001 /* Project object */; +} diff --git a/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131518 b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131518 new file mode 100644 index 0000000..fc541f1 --- /dev/null +++ b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131518 @@ -0,0 +1,232 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + 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 */; + }; + + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000064 /* PHPGuardApp.swift */; }; + 0000000000000000000000C9 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000065 /* Models.swift */; }; + 0000000000000000000000CA /* SSHScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000066 /* SSHScanner.swift */; }; + 0000000000000000000000CB /* AuditStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000067 /* AuditStore.swift */; }; + 0000000000000000000000CC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000068 /* ContentView.swift */; }; + B20000000000000000000001 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000001 /* SettingsView.swift */; }; + B20000000000000000000002 /* ReportPDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000002 /* ReportPDF.swift */; }; +/* 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; + path = VulnerabilityService.swift; + sourceTree = ""; + }; + + 000000000000000000000030 /* PHPGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PHPGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 000000000000000000000064 /* PHPGuardApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPGuardApp.swift; sourceTree = ""; }; + 000000000000000000000065 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 000000000000000000000066 /* SSHScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHScanner.swift; sourceTree = ""; }; + 000000000000000000000067 /* AuditStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditStore.swift; sourceTree = ""; }; + 000000000000000000000068 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B10000000000000000000001 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + B10000000000000000000002 /* ReportPDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportPDF.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 000000000000000000000010 = { + isa = PBXGroup; + children = ( + 000000000000000000000011 /* PHPGuard */, + 000000000000000000000012 /* Products */, + ); + sourceTree = ""; + }; + 000000000000000000000011 /* PHPGuard */ = { + isa = PBXGroup; + children = ( + 000000000000000000000064 /* PHPGuardApp.swift */, + 000000000000000000000065 /* Models.swift */, + 000000000000000000000066 /* SSHScanner.swift */, + 000000000000000000000067 /* AuditStore.swift */, + A10000000000000000000001 /* VulnerabilityService.swift */, + 000000000000000000000068 /* ContentView.swift */, + B10000000000000000000002 /* ReportPDF.swift */, + B10000000000000000000001 /* SettingsView.swift */, + ); + path = PHPGuard; + sourceTree = ""; + }; + 000000000000000000000012 /* Products */ = { + isa = PBXGroup; + children = ( + 000000000000000000000030 /* PHPGuard.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 000000000000000000000020 /* PHPGuard */ = { + isa = PBXNativeTarget; + buildConfigurationList = 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */; + buildPhases = ( + 000000000000000000000021 /* Sources */, + B10000000000000000000003 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PHPGuard; + productName = PHPGuard; + productReference = 000000000000000000000030 /* PHPGuard.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 000000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1600; + TargetAttributes = { + 000000000000000000000020 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 000000000000000000000010; + productRefGroup = 000000000000000000000012 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 000000000000000000000020 /* PHPGuard */, + ); + }; +/* 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; + buildActionMask = 2147483647; + files = ( + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */, + 0000000000000000000000C9 /* Models.swift in Sources */, + 0000000000000000000000CA /* SSHScanner.swift in Sources */, + 0000000000000000000000CB /* AuditStore.swift in Sources */, + 0000000000000000000000CC /* ContentView.swift in Sources */, + B20000000000000000000002 /* ReportPDF.swift in Sources */, + B20000000000000000000001 /* SettingsView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 000000000000000000000051 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000052 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 000000000000000000000053 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000054 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000051 /* Debug */, + 000000000000000000000052 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000053 /* Debug */, + 000000000000000000000054 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 000000000000000000000001 /* Project object */; +} diff --git a/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131723 b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131723 new file mode 100644 index 0000000..c9dab37 --- /dev/null +++ b/PHPGuard.xcodeproj/project.pbxproj.backup-20261022-131723 @@ -0,0 +1,232 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + C10000000000000000000002 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C10000000000000000000001 /* Assets.xcassets */; }; + A10000000000000000000002 /* VulnerabilityService.swift in Sources */ = { + isa = PBXBuildFile; + fileRef = A10000000000000000000001 /* VulnerabilityService.swift */; + }; + + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000064 /* PHPGuardApp.swift */; }; + 0000000000000000000000C9 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000065 /* Models.swift */; }; + 0000000000000000000000CA /* SSHScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000066 /* SSHScanner.swift */; }; + 0000000000000000000000CB /* AuditStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000067 /* AuditStore.swift */; }; + 0000000000000000000000CC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000068 /* ContentView.swift */; }; + B20000000000000000000001 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000001 /* SettingsView.swift */; }; + B20000000000000000000002 /* ReportPDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000000000000000000002 /* ReportPDF.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + C10000000000000000000001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A10000000000000000000001 /* VulnerabilityService.swift */ = { + isa = PBXFileReference; + lastKnownFileType = sourcecode.swift; + path = VulnerabilityService.swift; + sourceTree = ""; + }; + + 000000000000000000000030 /* PHPGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PHPGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 000000000000000000000064 /* PHPGuardApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPGuardApp.swift; sourceTree = ""; }; + 000000000000000000000065 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 000000000000000000000066 /* SSHScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHScanner.swift; sourceTree = ""; }; + 000000000000000000000067 /* AuditStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditStore.swift; sourceTree = ""; }; + 000000000000000000000068 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B10000000000000000000001 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + B10000000000000000000002 /* ReportPDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportPDF.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 000000000000000000000010 = { + isa = PBXGroup; + children = ( + 000000000000000000000011 /* PHPGuard */, + 000000000000000000000012 /* Products */, + ); + sourceTree = ""; + }; + 000000000000000000000011 /* PHPGuard */ = { + isa = PBXGroup; + children = ( + 000000000000000000000064 /* PHPGuardApp.swift */, + 000000000000000000000065 /* Models.swift */, + 000000000000000000000066 /* SSHScanner.swift */, + 000000000000000000000067 /* AuditStore.swift */, + A10000000000000000000001 /* VulnerabilityService.swift */, + 000000000000000000000068 /* ContentView.swift */, + B10000000000000000000002 /* ReportPDF.swift */, + B10000000000000000000001 /* SettingsView.swift */, + ); + path = PHPGuard; + sourceTree = ""; + }; + 000000000000000000000012 /* Products */ = { + isa = PBXGroup; + children = ( + 000000000000000000000030 /* PHPGuard.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 000000000000000000000020 /* PHPGuard */ = { + isa = PBXNativeTarget; + buildConfigurationList = 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */; + buildPhases = ( + 000000000000000000000021 /* Sources */, + B10000000000000000000003 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PHPGuard; + productName = PHPGuard; + productReference = 000000000000000000000030 /* PHPGuard.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 000000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1600; + TargetAttributes = { + 000000000000000000000020 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 000000000000000000000010; + productRefGroup = 000000000000000000000012 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 000000000000000000000020 /* PHPGuard */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B10000000000000000000003 /* Resources */ = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C10000000000000000000002 /* Assets.xcassets in Resources */, + ); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 000000000000000000000021 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0000000000000000000000C8 /* PHPGuardApp.swift in Sources */, + 0000000000000000000000C9 /* Models.swift in Sources */, + 0000000000000000000000CA /* SSHScanner.swift in Sources */, + 0000000000000000000000CB /* AuditStore.swift in Sources */, + 0000000000000000000000CC /* ContentView.swift in Sources */, + B20000000000000000000002 /* ReportPDF.swift in Sources */, + B20000000000000000000001 /* SettingsView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 000000000000000000000051 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000052 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 000000000000000000000053 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 000000000000000000000054 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 39R6U3437D; + ENABLE_APP_SANDBOX = NO; + GENERATE_INFOPLIST_FILE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_KEY_CFBundleDisplayName = PHPGuard; + MARKETING_VERSION = 1.9.0; + CURRENT_PROJECT_VERSION = 10; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PHPGuard; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 000000000000000000000041 /* Build configuration list for PBXProject "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000051 /* Debug */, + 000000000000000000000052 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 000000000000000000000042 /* Build configuration list for PBXNativeTarget "PHPGuard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000053 /* Debug */, + 000000000000000000000054 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 000000000000000000000001 /* Project object */; +} diff --git a/PHPGuard/AuditStore.swift b/PHPGuard/AuditStore.swift index bb783f2..1efbfa7 100644 --- a/PHPGuard/AuditStore.swift +++ b/PHPGuard/AuditStore.swift @@ -18,6 +18,12 @@ import SwiftUI return path } init() { + let defaults = UserDefaults.standard + autoRefreshCVEs = defaults.object(forKey: "phpguard.autoRefreshCVEs") as? Bool ?? true + showTechnicalDetails = defaults.bool(forKey: "phpguard.showTechnicalDetails") + confirmBeforeClearing = defaults.object(forKey: "phpguard.confirmClear") as? Bool ?? true + preferredReportAccent = defaults.string(forKey: "phpguard.reportAccent") ?? "Blue" + retainHistoryLimit = defaults.object(forKey: "phpguard.historyLimit") as? Int ?? 250 servers = load("servers.json") ?? [] results = (load("results.json") ?? []).map { Self.normalized($0) } if let cache: CVECache = load("cve-cache.json") { @@ -73,7 +79,7 @@ import SwiftUI save(CVECache(records: discoveredCVEs, fetchedAt: cveLastChecked), as: "cve-cache.json") } private func saveServers() { save(servers, as: "servers.json") } - private func saveResults() { save(Array(results.prefix(250)), as: "results.json") } + private func saveResults() { save(Array(results.prefix(max(1, retainHistoryLimit))), as: "results.json") } func latest(for server: Server) -> AuditResult? { results.first { $0.serverID == server.id } } func scan(_ server: Server) { guard !scanning.contains(server.id) else { return } @@ -92,7 +98,7 @@ import SwiftUI func scanAll() { servers.forEach { scan($0) } } func refreshCVEsOnLaunch() { // Preserve the local cache while checking NVD for a newer result set. - refreshCVEs() + if autoRefreshCVEs { refreshCVEs() } } func refreshCVEs() { guard !fetchingCVEs else { return } @@ -111,6 +117,22 @@ import SwiftUI } catch { cveError = error.localizedDescription } } } + @Published var showTechnicalDetails = false + @Published var confirmBeforeClearing = true + @Published var preferredReportAccent = "Blue" + @Published var autoRefreshCVEs = true + @Published var retainHistoryLimit = 250 + + func clearHistory() { results.removeAll() } + func removeHistory(_ offsets: IndexSet) { results.remove(atOffsets: offsets) } + func exportPDF(_ result: AuditResult) { + let panel = NSSavePanel() + panel.allowedContentTypes = [.pdf] + panel.nameFieldStringValue = "PHPGuard-\(result.serverName)-\(ISO8601DateFormatter().string(from: result.scannedAt)).pdf" + guard panel.runModal() == .OK, let url = panel.url else { return } + do { try ReportPDF.write(result, to: url, accent: preferredReportAccent) } + catch { lastError = "PDF export failed: \(error.localizedDescription)" } + } func export(_ result: AuditResult) { let panel = NSSavePanel() panel.allowedContentTypes = [.json] diff --git a/PHPGuard/ContentView.swift b/PHPGuard/ContentView.swift index 174db4a..f9d3a24 100644 --- a/PHPGuard/ContentView.swift +++ b/PHPGuard/ContentView.swift @@ -5,6 +5,8 @@ struct ContentView: View { @State private var selection: UUID? @State private var showingEditor = false @State private var editing: Server? + @State private var showClearHistoryConfirmation = false + @AppStorage("phpguard.confirmClear") private var confirmClearHistory = true private func addServer() { editing = nil; showingEditor = true } var body: some View { NavigationSplitView { @@ -110,7 +112,17 @@ struct ContentView: View { if let error = result.error { Label(error, systemImage: "xmark.circle.fill").foregroundStyle(.red) } else { - Text("Last scan: \(result.scannedAt.formatted())").foregroundStyle(.secondary) + HStack(spacing: 12) { + Label("Scan completed", systemImage: "checkmark.shield.fill") + .foregroundStyle(.green) + Text(result.scannedAt.formatted()).foregroundStyle(.secondary) + Spacer() + Button { store.exportPDF(result) } label: { Label("Export PDF", systemImage: "doc.richtext") } + .buttonStyle(.borderedProminent) + Button { store.export(result) } label: { Label("JSON", systemImage: "square.and.arrow.up") } + } + .padding(16) + .background(.green.opacity(0.08), in: RoundedRectangle(cornerRadius: 14)) Text("OS: \(result.os) • Host: \(result.hostname)") dataSection("PHP installations", result.phpVersions) dataSection("PHP-FPM services", result.fpmServices) @@ -123,15 +135,24 @@ struct ContentView: View { Text("Docker assessment: \(result.dockerAuditStatus)").foregroundStyle(.secondary) dataSection("Docker coverage (PHP CLI not detected)", result.dockerCoverage) dataSection("Security triage — follow-up actions", result.securityTriage) - Text("Findings").font(.title2.bold()) + HStack { + Text("Security findings").font(.title2.bold()) + Spacer() + Text("\(result.findings.count) total").foregroundStyle(.secondary) + } ForEach(result.findings) { finding in VStack(alignment: .leading, spacing: 4) { - Text("\(finding.severity): \(finding.title)").font(.headline) - Text(finding.detail).foregroundStyle(.secondary) + HStack(spacing: 10) { + Image(systemName: finding.severity.lowercased().contains("critical") || finding.severity.lowercased().contains("high") ? "exclamationmark.octagon.fill" : "info.circle.fill") + Text(finding.severity.uppercased()).font(.caption.bold()) + Text(finding.title).font(.headline) + } + .foregroundStyle(finding.severity.lowercased().contains("critical") || finding.severity.lowercased().contains("high") ? Color.red : (finding.severity.lowercased().contains("warn") || finding.severity.lowercased().contains("medium") ? Color.orange : Color.blue)) + Text(finding.detail).foregroundStyle(.secondary).textSelection(.enabled) }.padding(12).frame(maxWidth: .infinity, alignment: .leading) .background(.quaternary.opacity(0.3), in: RoundedRectangle(cornerRadius: 10)) } - Button("Export JSON report") { store.export(result) } + } } else { ContentUnavailableView("Not scanned", systemImage: "shield.lefthalf.filled", description: Text("Run a read-only SSH inventory scan.")) } }.padding(28).frame(maxWidth: .infinity, alignment: .leading) @@ -139,10 +160,18 @@ struct ContentView: View { } private func dataSection(_ title: String, _ values: [String]) -> some View { VStack(alignment: .leading, spacing: 6) { - Text(title).font(.title2.bold()) + HStack { + RoundedRectangle(cornerRadius: 2).fill(Color.accentColor).frame(width: 4, height: 22) + Text(title).font(.headline) + Spacer() + Text("\(values.count)").font(.caption).foregroundStyle(.secondary) + } if values.isEmpty { Text("None detected").foregroundStyle(.secondary) } else { ForEach(values, id: \.self) { Text($0).font(.system(.body, design: .monospaced)).textSelection(.enabled) } } } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.quaternary.opacity(0.22), in: RoundedRectangle(cornerRadius: 14)) } private var cveView: some View { VStack(alignment: .leading, spacing: 14) { @@ -177,18 +206,42 @@ struct ContentView: View { }.padding(24) } private var historyView: some View { - List(store.results) { result in + VStack(spacing: 0) { HStack { - VStack(alignment: .leading) { - Text(result.serverName).font(.headline) - Text(result.scannedAt.formatted()).foregroundStyle(.secondary) - if let error = result.error { Text(error).foregroundStyle(.red).lineLimit(2) } + VStack(alignment: .leading, spacing: 4) { + Text("Scan history").font(.largeTitle.bold()) + Text("\(store.results.count) saved scans").foregroundStyle(.secondary) } Spacer() - Button("Export") { store.export(result) }.disabled(result.error != nil) - }.padding(.vertical, 4) - }.navigationTitle("Scan history") + Button("Clear History", role: .destructive) { + if confirmClearHistory { showClearHistoryConfirmation = true } + else { store.clearHistory() } + }.disabled(store.results.isEmpty) + }.padding(20) + List { + ForEach(store.results) { result in + HStack(spacing: 12) { + Image(systemName: result.error == nil ? "checkmark.shield.fill" : "xmark.circle.fill") + .foregroundStyle(result.error == nil ? .green : .red) + VStack(alignment: .leading, spacing: 4) { + Text(result.serverName).font(.headline) + Text(result.scannedAt.formatted()).foregroundStyle(.secondary) + if let error = result.error { Text(error).foregroundStyle(.red).lineLimit(2) } + else { Text("\(result.findings.count) findings · \(result.phpVersions.count) PHP installations") + .font(.caption).foregroundStyle(.secondary) } + } + Spacer() + Button("PDF") { store.exportPDF(result) }.disabled(result.error != nil) + Button("JSON") { store.export(result) }.disabled(result.error != nil) + }.padding(.vertical, 5) + }.onDelete(perform: store.removeHistory) + } + } + .confirmationDialog("Clear all scan history?", isPresented: $showClearHistoryConfirmation) { + Button("Clear History", role: .destructive) { store.clearHistory() } + } message: { Text("Saved scans will be permanently removed. Servers and CVE downloads will remain.") } } + } struct ServerEditor: View { @@ -264,14 +317,4 @@ struct ServerEditor: View { } } -struct SettingsView: View { - var body: some View { - Form { - LabeledContent("SSH", value: "System OpenSSH, key-only") - LabeledContent("Host verification", value: "Strict") - LabeledContent("Scan mode", value: "Read-only") - Text("Server inventory and reports are stored locally in Application Support/PHPGuard.") - .foregroundStyle(.secondary) - }.padding(24).frame(width: 500, height: 220) - } -} + diff --git a/PHPGuard/ContentView.swift.backup-20261022-131636 b/PHPGuard/ContentView.swift.backup-20261022-131636 new file mode 100644 index 0000000..a250a62 --- /dev/null +++ b/PHPGuard/ContentView.swift.backup-20261022-131636 @@ -0,0 +1,330 @@ +import SwiftUI + +struct ContentView: View { + @EnvironmentObject var store: AuditStore + @State private var selection: UUID? + @State private var showingEditor = false + @State private var editing: Server? + @State private var showClearHistoryConfirmation = false + @AppStorage("phpguard.confirmClear") private var confirmClearHistory = true + private func addServer() { editing = nil; showingEditor = true } + var body: some View { + NavigationSplitView { + List(selection: $selection) { + Section("Servers") { + ForEach(store.servers) { server in + Label { + HStack { + Text(server.name) + Spacer() + if store.scanning.contains(server.id) { ProgressView().controlSize(.small) } + else if store.latest(for: server)?.error != nil { Image(systemName: "exclamationmark.triangle").foregroundStyle(.orange) } + } + } icon: { Image(systemName: "server.rack") } + .tag(server.id) + .contextMenu { + Button("Scan") { store.scan(server) } + Button("Edit") { editing = server; showingEditor = true } + Divider() + Button("Remove", role: .destructive) { + store.servers.removeAll { $0.id == server.id } + if selection == server.id { selection = nil } + } + } + } + } + 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")!) + } + } + .navigationTitle("PHPGuard") + .safeAreaInset(edge: .bottom) { + Button(action: addServer) { + Label("Add Server", systemImage: "plus.circle.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .padding(12) + .background(.bar) + } + } detail: { + 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) + } else { dashboard } + } + .sheet(isPresented: $showingEditor) { + ServerEditor(existing: editing) { server in + if let index = store.servers.firstIndex(where: { $0.id == server.id }) { store.servers[index] = server } + else { store.servers.append(server) } + selection = server.id + } + } + .alert("Scan error", isPresented: Binding(get: { store.lastError != nil }, set: { if !$0 { store.lastError = nil } })) { + Button("OK") { store.lastError = nil } + } message: { Text(store.lastError ?? "") } + } + private var dashboard: some View { + VStack(alignment: .leading, spacing: 24) { + Text("Security overview").font(.largeTitle.bold()) + HStack(spacing: 16) { + stat("Servers", "\(store.servers.count)", "server.rack") + stat("Scanned", "\(Set(store.results.filter { $0.error == nil }.map(\.serverID)).count)", "checkmark.shield") + stat("Findings", "\(store.results.filter { $0.error == nil }.prefix(store.servers.count).reduce(0) { $0 + $1.findings.count })", "exclamationmark.triangle") + } + Text("Read-only PHP inventory, APT upgrade checks and running Docker container discovery. Vendor advisory matching remains unimplemented; no server is certified secure.") + .foregroundStyle(.secondary) + HStack { + Button(action: addServer) { Label("Add Server", systemImage: "plus.circle.fill") } + .buttonStyle(.borderedProminent) + 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) + } + private func stat(_ title: String, _ value: String, _ icon: String) -> some View { + VStack(alignment: .leading, spacing: 12) { + Label(title, systemImage: icon).foregroundStyle(.secondary) + Text(value).font(.system(size: 36, weight: .bold)) + }.padding(20).frame(maxWidth: .infinity, alignment: .leading) + .background(.quaternary.opacity(0.35), in: RoundedRectangle(cornerRadius: 14)) + } + private func serverView(_ server: Server) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + HStack { + VStack(alignment: .leading) { + Text(server.name).font(.largeTitle.bold()) + Text("\(server.user)@\(server.host):\(server.port)").foregroundStyle(.secondary) + } + Spacer() + Button("Edit") { editing = server; showingEditor = true } + Button("Scan now") { store.scan(server) }.buttonStyle(.borderedProminent) + .disabled(store.scanning.contains(server.id)) + } + if let result = store.latest(for: server) { + if let error = result.error { + Label(error, systemImage: "xmark.circle.fill").foregroundStyle(.red) + } else { + HStack(spacing: 12) { + Label("Scan completed", systemImage: "checkmark.shield.fill") + .foregroundStyle(.green) + Text(result.scannedAt.formatted()).foregroundStyle(.secondary) + Spacer() + Button { store.exportPDF(result) } label: { Label("Export PDF", systemImage: "doc.richtext") } + .buttonStyle(.borderedProminent) + Button { store.export(result) } label: { Label("JSON", systemImage: "square.and.arrow.up") } + } + .padding(16) + .background(.green.opacity(0.08), in: RoundedRectangle(cornerRadius: 14)) + Text("OS: \(result.os) • Host: \(result.hostname)") + dataSection("PHP installations", result.phpVersions) + dataSection("PHP-FPM services", result.fpmServices) + dataSection("Nginx FastCGI routes", result.nginxUpstreams) + dataSection("Installed packages", result.packageVersions) + dataSection("Available PHP package upgrades", result.availableUpdates) + dataSection("APT package origins and candidate revisions", result.packageOrigins) + Text("Package assessment: \(result.packageAuditStatus)").foregroundStyle(.secondary) + dataSection("PHP detected in running Docker containers", result.dockerPHP) + Text("Docker assessment: \(result.dockerAuditStatus)").foregroundStyle(.secondary) + dataSection("Docker coverage (PHP CLI not detected)", result.dockerCoverage) + dataSection("Security triage — follow-up actions", result.securityTriage) + HStack { + Text("Security findings").font(.title2.bold()) + Spacer() + Text("\(result.findings.count) total").foregroundStyle(.secondary) + } + ForEach(result.findings) { finding in + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 10) { + Image(systemName: finding.severity.lowercased().contains("critical") || finding.severity.lowercased().contains("high") ? "exclamationmark.octagon.fill" : "info.circle.fill") + Text(finding.severity.uppercased()).font(.caption.bold()) + Text(finding.title).font(.headline) + } + .foregroundStyle(finding.severity.lowercased().contains("critical") || finding.severity.lowercased().contains("high") ? Color.red : (finding.severity.lowercased().contains("warn") || finding.severity.lowercased().contains("medium") ? Color.orange : Color.blue)) + Text(finding.detail).foregroundStyle(.secondary).textSelection(.enabled) + }.padding(12).frame(maxWidth: .infinity, alignment: .leading) + .background(.quaternary.opacity(0.3), in: RoundedRectangle(cornerRadius: 10)) + } + + } + } else { ContentUnavailableView("Not scanned", systemImage: "shield.lefthalf.filled", description: Text("Run a read-only SSH inventory scan.")) } + }.padding(28).frame(maxWidth: .infinity, alignment: .leading) + } + } + private func dataSection(_ title: String, _ values: [String]) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + RoundedRectangle(cornerRadius: 2).fill(Color.accentColor).frame(width: 4, height: 22) + Text(title).font(.headline) + Spacer() + Text("\(values.count)").font(.caption).foregroundStyle(.secondary) + } + if values.isEmpty { Text("None detected").foregroundStyle(.secondary) } + else { ForEach(values, id: \.self) { Text($0).font(.system(.body, design: .monospaced)).textSelection(.enabled) } } + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.quaternary.opacity(0.22), in: RoundedRectangle(cornerRadius: 14)) + } + 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 updated: \(date.formatted()) · Saved locally · Automatically refreshed at launch") + .foregroundStyle(.secondary) + } else { + Text("No cached CVE research yet · Automatically fetched at launch") + .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 { + VStack(spacing: 0) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Scan history").font(.largeTitle.bold()) + Text("\(store.results.count) saved scans").foregroundStyle(.secondary) + } + Spacer() + Button("Clear History", role: .destructive) { + if confirmClearHistory { showClearHistoryConfirmation = true } + else { store.clearHistory() } + }.disabled(store.results.isEmpty) + }.padding(20) + List { + ForEach(store.results) { result in + HStack(spacing: 12) { + Image(systemName: result.error == nil ? "checkmark.shield.fill" : "xmark.circle.fill") + .foregroundStyle(result.error == nil ? .green : .red) + VStack(alignment: .leading, spacing: 4) { + Text(result.serverName).font(.headline) + Text(result.scannedAt.formatted()).foregroundStyle(.secondary) + if let error = result.error { Text(error).foregroundStyle(.red).lineLimit(2) } + else { Text("\(result.findings.count) findings · \(result.phpVersions.count) PHP installations") + .font(.caption).foregroundStyle(.secondary) } + } + Spacer() + Button("PDF") { store.exportPDF(result) }.disabled(result.error != nil) + Button("JSON") { store.export(result) }.disabled(result.error != nil) + }.padding(.vertical, 5) + }.onDelete(perform: store.removeHistory) + } + } + .confirmationDialog("Clear all scan history?", isPresented: $showClearHistoryConfirmation) { + Button("Clear History", role: .destructive) { store.clearHistory() } + } message: { Text("Saved scans will be permanently removed. Servers and CVE downloads will remain.") } + } + +} + +struct ServerEditor: View { + @Environment(\.dismiss) private var dismiss + let existing: Server? + let onSave: (Server) -> Void + @State private var server: Server + init(existing: Server?, onSave: @escaping (Server) -> Void) { + self.existing = existing + self.onSave = onSave + _server = State(initialValue: existing ?? Server(name: "", host: "")) + } + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Label(existing == nil ? "Add a server" : "Edit server", systemImage: "server.rack") + .font(.title.bold()) + Text("Connect using an existing SSH key. No agent installation required.") + .foregroundStyle(.secondary) + Form { + Section("Connection") { + TextField("Server name (e.g. Pegasus)", text: $server.name) + TextField("Hostname or IP address", text: $server.host) + .textContentType(.URL) + TextField("SSH username", text: $server.user) + TextField("SSH port", value: $server.port, format: .number) + } + Section("Advanced (optional)") { + HStack { + TextField("SSH private key path", text: $server.identityFile) + Button("Choose…") { + let panel = NSOpenPanel() + panel.canChooseDirectories = false + panel.canChooseFiles = true + if panel.runModal() == .OK, let url = panel.url { + server.identityFile = url.path + } + } + } + Text("Leave blank to use your existing SSH configuration and keys.") + .font(.caption).foregroundStyle(.secondary) + } + }.formStyle(.grouped) + if !isValid { + Label("Enter a valid server name, SSH host, username and port (1–65535).", systemImage: "info.circle") + .foregroundStyle(.secondary).font(.caption) + } + Text("Host key verification is strict. Connect in Terminal first if this server is new.") + .font(.caption).foregroundStyle(.secondary) + HStack { + Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction) + Spacer() + Button(existing == nil ? "Add Server" : "Save Changes") { + server.name = server.name.trimmingCharacters(in: .whitespacesAndNewlines) + server.host = server.host.trimmingCharacters(in: .whitespacesAndNewlines) + server.user = server.user.trimmingCharacters(in: .whitespacesAndNewlines) + onSave(server) + dismiss() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + .disabled(!isValid) + } + }.padding(24).frame(width: 580) + } + private var isValid: Bool { + let host = server.host.trimmingCharacters(in: .whitespacesAndNewlines) + let user = server.user.trimmingCharacters(in: .whitespacesAndNewlines) + return !server.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + && !host.isEmpty && !host.hasPrefix("-") + && host.range(of: "^[A-Za-z0-9][A-Za-z0-9.:-]*$", options: .regularExpression) != nil + && user.range(of: "^[A-Za-z0-9_][A-Za-z0-9_.-]*$", options: .regularExpression) != nil + && (1...65535).contains(server.port) + } +} + +struct SettingsView: View { + var body: some View { + Form { + LabeledContent("SSH", value: "System OpenSSH, key-only") + LabeledContent("Host verification", value: "Strict") + LabeledContent("Scan mode", value: "Read-only") + Text("Server inventory and reports are stored locally in Application Support/PHPGuard.") + .foregroundStyle(.secondary) + }.padding(24).frame(width: 500, height: 220) + } +} diff --git a/PHPGuard/ReportPDF.swift b/PHPGuard/ReportPDF.swift new file mode 100644 index 0000000..8f112c0 --- /dev/null +++ b/PHPGuard/ReportPDF.swift @@ -0,0 +1,1157 @@ + +import AppKit +import CoreGraphics + +@MainActor +enum ReportPDF { + + static func write( + _ report: AuditResult, + to url: URL, + accent: String + ) throws { + + let renderer = SecurityReportRenderer( + report: report, + accent: accent + ) + + try renderer.generate(to: url) + } +} + +@MainActor +private final class SecurityReportRenderer { + + private let report: AuditResult + + private let pageWidth: CGFloat = 595 + private let pageHeight: CGFloat = 842 + private let margin: CGFloat = 48 + private let contentWidth: CGFloat = 499 + + private let navy = NSColor( + calibratedRed: 0.055, + green: 0.090, + blue: 0.155, + alpha: 1 + ) + + private let ink = NSColor( + calibratedRed: 0.12, + green: 0.17, + blue: 0.25, + alpha: 1 + ) + + private let muted = NSColor( + calibratedRed: 0.40, + green: 0.46, + blue: 0.54, + alpha: 1 + ) + + private let pale = NSColor( + calibratedRed: 0.955, + green: 0.965, + blue: 0.980, + alpha: 1 + ) + + private let accentColor: NSColor + + private var context: CGContext! + private var y: CGFloat = 0 + private var pageNumber = 0 + private var pageOpen = false + + init(report: AuditResult, accent: String) { + self.report = report + + switch accent { + case "Teal": + accentColor = NSColor( + calibratedRed: 0.00, + green: 0.66, + blue: 0.62, + alpha: 1 + ) + case "Purple": + accentColor = NSColor( + calibratedRed: 0.47, + green: 0.37, + blue: 0.84, + alpha: 1 + ) + default: + accentColor = NSColor( + calibratedRed: 0.20, + green: 0.43, + blue: 0.86, + alpha: 1 + ) + } + } + + private func font( + _ size: CGFloat, + weight: NSFont.Weight = .regular + ) -> NSFont { + NSFont.systemFont(ofSize: size, weight: weight) + } + + private func attributes( + size: CGFloat, + weight: NSFont.Weight = .regular, + color: NSColor = .black + ) -> [NSAttributedString.Key: Any] { + + let paragraph = NSMutableParagraphStyle() + paragraph.lineSpacing = 3 + paragraph.lineBreakMode = .byCharWrapping + + return [ + .font: font(size, weight: weight), + .foregroundColor: color, + .paragraphStyle: paragraph + ] + } + + private func textHeight( + _ text: String, + width: CGFloat, + size: CGFloat, + weight: NSFont.Weight = .regular + ) -> CGFloat { + + let value = NSAttributedString( + string: text, + attributes: attributes( + size: size, + weight: weight + ) + ) + + let bounds = value.boundingRect( + with: CGSize( + width: width, + height: 100000 + ), + options: [ + .usesLineFragmentOrigin, + .usesFontLeading + ] + ) + + return max(size + 5, ceil(bounds.height) + 4) + } + + private func drawText( + _ text: String, + x: CGFloat, + y position: CGFloat, + width: CGFloat, + size: CGFloat = 10, + weight: NSFont.Weight = .regular, + color: NSColor = .black + ) -> CGFloat { + + let value = NSAttributedString( + string: text, + attributes: attributes( + size: size, + weight: weight, + color: color + ) + ) + + let height = textHeight( + text, + width: width, + size: size, + weight: weight + ) + + value.draw( + with: CGRect( + x: x, + y: position, + width: width, + height: height + ), + options: [ + .usesLineFragmentOrigin, + .usesFontLeading + ] + ) + + return height + } + + private func rectangle( + x: CGFloat, + y: CGFloat, + width: CGFloat, + height: CGFloat, + color: NSColor, + radius: CGFloat = 0 + ) { + + let rect = CGRect( + x: x, + y: y, + width: width, + height: height + ) + + let path = NSBezierPath( + roundedRect: rect, + xRadius: radius, + yRadius: radius + ) + + color.setFill() + path.fill() + } + + private func rule( + at position: CGFloat, + color: NSColor? = nil + ) { + + rectangle( + x: margin, + y: position, + width: contentWidth, + height: 1, + color: color ?? NSColor( + calibratedWhite: 0.88, + alpha: 1 + ) + ) + } + + private func withGraphics( + _ operation: () -> Void + ) { + + context.saveGState() + NSGraphicsContext.saveGraphicsState() + + context.translateBy(x: 0, y: pageHeight) + context.scaleBy(x: 1, y: -1) + + NSGraphicsContext.current = NSGraphicsContext( + cgContext: context, + flipped: true + ) + + operation() + + NSGraphicsContext.restoreGraphicsState() + context.restoreGState() + } + + private func beginPage( + title: String = "SECURITY ASSESSMENT" + ) { + + context.beginPDFPage(nil) + pageOpen = true + pageNumber += 1 + y = 105 + + withGraphics { + + rectangle( + x: 0, + y: 0, + width: pageWidth, + height: 9, + color: accentColor + ) + + _ = drawText( + "PHPGUARD", + x: margin, + y: 29, + width: 200, + size: 15, + weight: .bold, + color: navy + ) + + _ = drawText( + title.uppercased(), + x: 290, + y: 33, + width: 257, + size: 8, + weight: .semibold, + color: muted + ) + + rule(at: 70) + } + } + + private func finishPage() { + + guard pageOpen else { return } + + withGraphics { + + rule(at: 790) + + _ = drawText( + "PHPGuard / \(report.serverName)", + x: margin, + y: 802, + width: 280, + size: 8, + color: muted + ) + + _ = drawText( + "ASSESSMENT • \(pageNumber)", + x: 390, + y: 802, + width: 157, + size: 8, + weight: .medium, + color: muted + ) + } + + context.endPDFPage() + pageOpen = false + } + + private func ensureSpace( + _ required: CGFloat, + title: String = "SECURITY ASSESSMENT" + ) { + + if y + required > 775 { + finishPage() + beginPage(title: title) + } + } + + private func heading( + _ title: String, + subtitle: String? = nil + ) { + + let subtitleHeight = subtitle.map { + textHeight($0, width: contentWidth, size: 9) + } ?? 0 + + ensureSpace(65 + subtitleHeight) + + withGraphics { + + rectangle( + x: margin, + y: y + 3, + width: 4, + height: 24, + color: accentColor, + radius: 2 + ) + + _ = drawText( + title, + x: margin + 16, + y: y, + width: contentWidth - 16, + size: 18, + weight: .bold, + color: navy + ) + + if let subtitle { + _ = drawText( + subtitle, + x: margin + 16, + y: y + 30, + width: contentWidth - 16, + size: 9, + color: muted + ) + } + } + + y += 45 + subtitleHeight + } + + private func sectionTitle(_ title: String) { + + ensureSpace(42) + + withGraphics { + _ = drawText( + title.uppercased(), + x: margin, + y: y, + width: contentWidth, + size: 10, + weight: .bold, + color: accentColor + ) + } + + y += 25 + } + + private func paragraph( + _ text: String, + size: CGFloat = 10, + color: NSColor? = nil + ) { + + let width = contentWidth + + let height = textHeight( + text, + width: width, + size: size + ) + + ensureSpace(height + 10) + + withGraphics { + _ = drawText( + text, + x: margin, + y: y, + width: width, + size: size, + color: color ?? ink + ) + } + + y += height + 10 + } + + private func detailRow( + label: String, + value: String + ) { + + let labelWidth: CGFloat = 145 + let valueWidth = contentWidth - labelWidth - 20 + + let height = max( + textHeight( + label, + width: labelWidth, + size: 9 + ), + textHeight( + value, + width: valueWidth, + size: 9 + ) + ) + 18 + + ensureSpace(height) + + withGraphics { + + rectangle( + x: margin, + y: y, + width: contentWidth, + height: height - 2, + color: pale, + radius: 5 + ) + + _ = drawText( + label, + x: margin + 12, + y: y + 9, + width: labelWidth, + size: 9, + weight: .semibold, + color: muted + ) + + _ = drawText( + value, + x: margin + labelWidth + 14, + y: y + 9, + width: valueWidth - 14, + size: 9, + color: ink + ) + } + + y += height + 3 + } + + private func evidence( + _ items: [String], + empty: String = "No entries recorded" + ) { + + if items.isEmpty { + paragraph(empty, size: 9, color: muted) + return + } + + for (index, item) in items.enumerated() { + + let width = contentWidth - 32 + + let height = textHeight( + item, + width: width, + size: 9 + ) + 14 + + ensureSpace(height) + + withGraphics { + + if index.isMultiple(of: 2) { + rectangle( + x: margin, + y: y, + width: contentWidth, + height: height, + color: pale, + radius: 4 + ) + } + + rectangle( + x: margin + 9, + y: y + 12, + width: 4, + height: 4, + color: accentColor, + radius: 2 + ) + + _ = drawText( + item, + x: margin + 22, + y: y + 7, + width: width, + size: 9, + color: ink + ) + } + + y += height + 3 + } + + y += 10 + } + + private func severityColor( + _ severity: String + ) -> NSColor { + + switch severity.lowercased() { + + case "critical": + return .systemRed + + case "high": + return NSColor( + calibratedRed: 0.83, + green: 0.25, + blue: 0.23, + alpha: 1 + ) + + case "medium", "warning": + return .systemOrange + + case "low": + return .systemYellow + + default: + return accentColor + } + } + + private func findingCard(_ finding: Finding) { + + let detailHeight = textHeight( + finding.detail, + width: contentWidth - 32, + size: 9 + ) + + let titleHeight = textHeight( + finding.title, + width: contentWidth - 140, + size: 11, + weight: .semibold + ) + + let cardHeight = 65 + titleHeight + detailHeight + + ensureSpace(cardHeight + 12, title: "SECURITY FINDINGS") + + withGraphics { + + rectangle( + x: margin, + y: y, + width: contentWidth, + height: cardHeight, + color: pale, + radius: 9 + ) + + rectangle( + x: margin, + y: y, + width: 5, + height: cardHeight, + color: severityColor(finding.severity), + radius: 2 + ) + + _ = drawText( + finding.title, + x: margin + 18, + y: y + 15, + width: contentWidth - 145, + size: 11, + weight: .semibold, + color: navy + ) + + _ = drawText( + finding.severity.uppercased(), + x: margin + contentWidth - 112, + y: y + 17, + width: 95, + size: 9, + weight: .bold, + color: severityColor(finding.severity) + ) + + _ = drawText( + finding.detail, + x: margin + 18, + y: y + 29 + titleHeight, + width: contentWidth - 36, + size: 9, + color: ink + ) + } + + y += cardHeight + 12 + } + + private func metric( + _ value: String, + label: String, + x: CGFloat, + width: CGFloat + ) { + + rectangle( + x: x, + y: 445, + width: width, + height: 104, + color: NSColor( + calibratedRed: 0.10, + green: 0.15, + blue: 0.23, + alpha: 1 + ), + radius: 10 + ) + + _ = drawText( + value, + x: x + 16, + y: 463, + width: width - 32, + size: 29, + weight: .bold, + color: .white + ) + + _ = drawText( + label.uppercased(), + x: x + 16, + y: 514, + width: width - 32, + size: 8, + weight: .medium, + color: NSColor( + calibratedWhite: 0.72, + alpha: 1 + ) + ) + } + + private func cover() { + + context.beginPDFPage(nil) + pageOpen = true + pageNumber += 1 + + withGraphics { + + rectangle( + x: 0, + y: 0, + width: pageWidth, + height: pageHeight, + color: navy + ) + + rectangle( + x: 0, + y: 0, + width: pageWidth, + height: 12, + color: accentColor + ) + + _ = drawText( + "PHPGUARD", + x: margin, + y: 57, + width: 350, + size: 23, + weight: .heavy, + color: .white + ) + + _ = drawText( + "SERVER SECURITY INTELLIGENCE", + x: margin, + y: 94, + width: 450, + size: 9, + weight: .medium, + color: accentColor + ) + + rectangle( + x: margin, + y: 160, + width: 52, + height: 4, + color: accentColor, + radius: 2 + ) + + _ = drawText( + "SECURITY\nASSESSMENT", + x: margin, + y: 188, + width: contentWidth, + size: 36, + weight: .bold, + color: .white + ) + + _ = drawText( + report.serverName, + x: margin, + y: 310, + width: contentWidth, + size: 24, + weight: .semibold, + color: .white + ) + + _ = drawText( + report.hostname, + x: margin, + y: 350, + width: contentWidth, + size: 12, + color: NSColor( + calibratedWhite: 0.70, + alpha: 1 + ) + ) + + rectangle( + x: margin, + y: 392, + width: contentWidth, + height: 35, + color: NSColor( + calibratedRed: 0.20, + green: 0.24, + blue: 0.32, + alpha: 1 + ), + radius: 6 + ) + + _ = drawText( + "ASSESSMENT STATUS: VERIFICATION REQUIRED", + x: margin + 13, + y: 403, + width: contentWidth - 26, + size: 10, + weight: .bold, + color: .white + ) + + let gap: CGFloat = 10 + let metricWidth = (contentWidth - 2 * gap) / 3 + + metric( + "\(report.phpVersions.count)", + label: "PHP runtimes", + x: margin, + width: metricWidth + ) + + metric( + "\(report.findings.count)", + label: "Findings", + x: margin + metricWidth + gap, + width: metricWidth + ) + + metric( + "\(report.availableUpdates.count)", + label: "Updates recorded", + x: margin + (metricWidth + gap) * 2, + width: metricWidth + ) + + _ = drawText( + "ASSESSMENT DETAILS", + x: margin, + y: 598, + width: contentWidth, + size: 10, + weight: .bold, + color: accentColor + ) + + _ = drawText( + "Operating system\n\(report.os)", + x: margin, + y: 626, + width: contentWidth, + size: 11, + color: .white + ) + + _ = drawText( + "Assessment date\n\(report.scannedAt.formatted(date: .abbreviated, time: .standard))", + x: margin, + y: 686, + width: contentWidth, + size: 11, + color: .white + ) + + _ = drawText( + "CVE exposure is not verified by version inventory alone.", + x: margin, + y: 775, + width: contentWidth, + size: 9, + color: NSColor( + calibratedWhite: 0.72, + alpha: 1 + ) + ) + } + + context.endPDFPage() + pageOpen = false + } + + private func executiveSummary() { + + beginPage(title: "EXECUTIVE SUMMARY") + + heading( + "Executive Summary", + subtitle: "Assessment overview and key observations" + ) + + paragraph( + "PHPGuard collected PHP runtime, package, service and security investigation evidence from \(report.serverName). The results represent the checks completed during this scan, not a guarantee that the server is secure." + ) + + sectionTitle("Assessment Overview") + + detailRow( + label: "Server", + value: report.serverName + ) + + detailRow( + label: "Hostname", + value: report.hostname + ) + + detailRow( + label: "Operating system", + value: report.os + ) + + detailRow( + label: "PHP runtimes", + value: "\(report.phpVersions.count) detected" + ) + + detailRow( + label: "Security findings", + value: "\(report.findings.count) recorded" + ) + + detailRow( + label: "Package updates", + value: "\(report.availableUpdates.count) recorded" + ) + + sectionTitle("Security Assessment") + + paragraph( + "Verification required. PHP package versions and available updates do not independently establish CVE exposure. Vendor security advisories, package origins and backported fixes must be checked before determining vulnerability status." + ) + + sectionTitle("Investigation Coverage") + + detailRow( + label: "Package assessment", + value: report.packageAuditStatus + ) + + detailRow( + label: "Docker assessment", + value: report.dockerAuditStatus + ) + + detailRow( + label: "Investigation", + value: report.investigationStatus + ) + + finishPage() + } + + private func findingsSection() { + + beginPage(title: "SECURITY FINDINGS") + + heading( + "Security Findings", + subtitle: "\(report.findings.count) observations recorded during the assessment" + ) + + if report.findings.isEmpty { + + paragraph( + "No findings were recorded by the completed checks. This does not establish that the server is free from vulnerabilities." + ) + + } else { + + for finding in report.findings { + findingCard(finding) + } + } + + sectionTitle("Security Triage") + + evidence( + report.securityTriage, + empty: "No additional triage guidance recorded." + ) + + finishPage() + } + + private func environmentSection() { + + beginPage(title: "PHP ENVIRONMENT") + + heading( + "PHP Environment", + subtitle: "Runtime, service and web-server configuration" + ) + + sectionTitle("Detected PHP Installations") + evidence(report.phpVersions) + + sectionTitle("PHP-FPM Services") + evidence(report.fpmServices) + + sectionTitle("Nginx FastCGI Configuration") + evidence(report.nginxUpstreams) + + sectionTitle("Available Package Updates") + evidence( + report.availableUpdates, + empty: "No available package updates were recorded." + ) + + finishPage() + } + + private func coverageSection() { + + beginPage(title: "SECURITY COVERAGE") + + heading( + "Security Coverage", + subtitle: "Assessment limitations and recommended follow-up" + ) + + sectionTitle("Coverage Status") + + detailRow( + label: "Package inventory", + value: report.packageAuditStatus + ) + + detailRow( + label: "Container coverage", + value: report.dockerAuditStatus + ) + + detailRow( + label: "Investigation", + value: report.investigationStatus + ) + + sectionTitle("Recommended Actions") + + evidence([ + "Verify installed PHP package revisions against applicable Ubuntu Security Notices or third-party vendor advisories.", + "Review each recorded security finding and determine whether the observed condition requires remediation.", + "Investigate unavailable or incomplete checks before treating the assessment as comprehensive.", + "Review PHP-FPM services and confirm that configured Nginx upstreams match the intended active runtimes.", + "Review recently modified PHP files against expected application deployments and maintenance activity.", + "If PHP containers are present, assess their runtime versions and image updates independently of host packages." + ]) + + finishPage() + } + + private func appendixSection( + title: String, + subtitle: String, + items: [String], + empty: String = "No entries recorded" + ) { + + heading(title, subtitle: subtitle) + evidence(items, empty: empty) + } + + private func appendix() { + + beginPage(title: "TECHNICAL APPENDIX") + + heading( + "Technical Appendix", + subtitle: "Supporting evidence collected during the scan" + ) + + paragraph( + "The following entries preserve the collected scan evidence. Package inventories, paths and service output may require further interpretation." + ) + + appendixSection( + title: "Installed Packages", + subtitle: "Detected PHP-related package inventory", + items: report.packageVersions + ) + + appendixSection( + title: "APT Package Origins", + subtitle: "Installed versions, candidates and repository evidence", + items: report.packageOrigins + ) + + appendixSection( + title: "Docker PHP", + subtitle: "Container runtime inventory", + items: report.dockerPHP + ) + + appendixSection( + title: "Docker Coverage", + subtitle: "Container investigation evidence", + items: report.dockerCoverage + ) + + appendixSection( + title: "Recently Modified PHP Files", + subtitle: "\(report.recentPHPFiles.count) file entries recorded", + items: report.recentPHPFiles + ) + + appendixSection( + title: "Web Error Signals", + subtitle: "Recorded application and web-server observations", + items: report.webErrorSignals + ) + + finishPage() + } + + func generate(to url: URL) throws { + + let data = NSMutableData() + + guard let consumer = CGDataConsumer( + data: data as CFMutableData + ) else { + throw NSError( + domain: "PHPGuardPDF", + code: 1 + ) + } + + var mediaBox = CGRect( + x: 0, + y: 0, + width: pageWidth, + height: pageHeight + ) + + guard let pdfContext = CGContext( + consumer: consumer, + mediaBox: &mediaBox, + nil + ) else { + throw NSError( + domain: "PHPGuardPDF", + code: 2 + ) + } + + context = pdfContext + + cover() + executiveSummary() + findingsSection() + environmentSection() + coverageSection() + appendix() + + context.closePDF() + + try (data as Data).write( + to: url, + options: .atomic + ) + } +} diff --git a/PHPGuard/ReportPDF.swift.backup-20261022-132039 b/PHPGuard/ReportPDF.swift.backup-20261022-132039 new file mode 100644 index 0000000..864eac7 --- /dev/null +++ b/PHPGuard/ReportPDF.swift.backup-20261022-132039 @@ -0,0 +1,64 @@ +import AppKit +import CoreGraphics + +@MainActor enum ReportPDF { + static func write(_ report: AuditResult, to url: URL, accent: String) throws { + let data = NSMutableData() + guard let consumer = CGDataConsumer(data: data as CFMutableData) else { throw NSError(domain: "PHPGuardPDF", code: 1) } + var box = CGRect(x: 0, y: 0, width: 595, height: 842) + guard let ctx = CGContext(consumer: consumer, mediaBox: &box, nil) else { throw NSError(domain: "PHPGuardPDF", code: 2) } + let primary: NSColor = accent == "Teal" ? .systemTeal : accent == "Purple" ? .systemPurple : .systemBlue + let margin: CGFloat = 44 + var y: CGFloat = 0 + var page = 0 + func startPage() { + ctx.beginPDFPage(nil); page += 1; y = 48 + ctx.setFillColor(primary.cgColor) + ctx.fill(CGRect(x: 0, y: 0, width: 595, height: 13)) + let heading = NSAttributedString(string: "PHPGUARD / SECURITY ASSESSMENT", attributes: [.font: NSFont.boldSystemFont(ofSize: 10), .foregroundColor: primary]) + ctx.saveGState(); NSGraphicsContext.saveGraphicsState(); NSGraphicsContext.current = NSGraphicsContext(cgContext: ctx, flipped: true); ctx.translateBy(x: 0, y: 842); ctx.scaleBy(x: 1, y: -1) + heading.draw(at: CGPoint(x: margin, y: 25)) + NSGraphicsContext.restoreGraphicsState(); ctx.restoreGState() + } + func endPage() { + let footer = NSAttributedString(string: "PHPGuard • Research findings require vendor advisory verification Page \(page)", attributes: [.font: NSFont.systemFont(ofSize: 8), .foregroundColor: NSColor.secondaryLabelColor]) + ctx.saveGState(); NSGraphicsContext.saveGraphicsState(); NSGraphicsContext.current = NSGraphicsContext(cgContext: ctx, flipped: true); ctx.translateBy(x: 0, y: 842); ctx.scaleBy(x: 1, y: -1) + footer.draw(at: CGPoint(x: margin, y: 808)) + NSGraphicsContext.restoreGraphicsState(); ctx.restoreGState(); ctx.endPDFPage() + } + func line(_ text: String, size: CGFloat = 10, bold: Bool = false, color: NSColor = .labelColor) { + let attrs: [NSAttributedString.Key: Any] = [.font: bold ? NSFont.boldSystemFont(ofSize: size) : NSFont.systemFont(ofSize: size), .foregroundColor: color] + let value = NSAttributedString(string: text, attributes: attrs) + let bound = value.boundingRect(with: CGSize(width: 507, height: 10000), options: [.usesLineFragmentOrigin, .usesFontLeading]) + let height = max(size + 5, ceil(bound.height) + 5) + if y + height > 790 { endPage(); startPage() } + ctx.saveGState(); NSGraphicsContext.saveGraphicsState(); NSGraphicsContext.current = NSGraphicsContext(cgContext: ctx, flipped: true); ctx.translateBy(x: 0, y: 842); ctx.scaleBy(x: 1, y: -1) + value.draw(with: CGRect(x: margin, y: y, width: 507, height: height), options: [.usesLineFragmentOrigin, .usesFontLeading]) + NSGraphicsContext.restoreGraphicsState(); ctx.restoreGState(); y += height + } + func section(_ title: String, _ rows: [String]) { + y += 14; line(title.uppercased(), size: 12, bold: true, color: primary) + if rows.isEmpty { line("No entries recorded", color: .secondaryLabelColor) } + else { for row in rows { line("• " + row) } } + } + startPage() + y += 18; line(report.serverName, size: 25, bold: true) + line("Host: \(report.hostname) OS: \(report.os)", color: .secondaryLabelColor) + line("Scan date: \(report.scannedAt.formatted(date: .abbreviated, time: .standard))", color: .secondaryLabelColor) + section("Executive summary", ["\(report.phpVersions.count) PHP runtimes • \(report.findings.count) findings • \(report.availableUpdates.count) package updates", "CVE status is unverified until matched with vendor advisories."]) + section("PHP installations", report.phpVersions) + section("PHP-FPM services", report.fpmServices) + section("Available package updates", report.availableUpdates) + section("Security findings", report.findings.map { "[\($0.severity)] \($0.title): \($0.detail)" }) + section("Security triage", report.securityTriage) + section("Nginx FastCGI", report.nginxUpstreams) + section("Installed packages", report.packageVersions) + section("APT package origins", report.packageOrigins) + section("Docker PHP", report.dockerPHP) + section("Docker coverage", report.dockerCoverage) + section("Recent PHP files", report.recentPHPFiles) + section("Web error signals", report.webErrorSignals) + section("Assessment notes", [report.packageAuditStatus, report.dockerAuditStatus, report.investigationStatus]) + endPage(); ctx.closePDF(); try (data as Data).write(to: url, options: .atomic) + } +} diff --git a/PHPGuard/SettingsView.swift b/PHPGuard/SettingsView.swift new file mode 100644 index 0000000..fffe4c6 --- /dev/null +++ b/PHPGuard/SettingsView.swift @@ -0,0 +1,67 @@ +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject private var store: AuditStore + @AppStorage("phpguard.autoRefreshCVEs") private var autoRefresh = true + @AppStorage("phpguard.showTechnicalDetails") private var technical = false + @AppStorage("phpguard.confirmClear") private var confirmClear = true + @AppStorage("phpguard.reportAccent") private var accent = "Blue" + @AppStorage("phpguard.historyLimit") private var historyLimit = 250 + @State private var showClearConfirmation = false + + var body: some View { + TabView { + Form { + Section("Vulnerability intelligence") { + Toggle("Refresh CVE research when PHPGuard starts", isOn: $autoRefresh) + LabeledContent("Cached CVEs", value: "\(store.discoveredCVEs.count)") + LabeledContent("Last refreshed", value: store.cveLastChecked?.formatted() ?? "Never") + Button("Refresh CVEs now") { store.refreshCVEs() }.disabled(store.fetchingCVEs) + Text("NVD keyword matches are research leads, not verified server vulnerabilities.") + .font(.caption).foregroundStyle(.secondary) + } + }.formStyle(.grouped).tabItem { Label("Intelligence", systemImage: "shield.lefthalf.filled") } + Form { + Section("Reports") { + Picker("PDF accent", selection: $accent) { + ForEach(["Blue", "Teal", "Purple"], id: \.self) { Text($0) } + } + Toggle("Show technical details in reports", isOn: $technical) + } + Section("History") { + Stepper("Retain up to \(historyLimit) scans", value: $historyLimit, in: 25...2000, step: 25) + Toggle("Confirm before clearing scan history", isOn: $confirmClear) + Button("Clear scan history", role: .destructive) { + if confirmClear { showClearConfirmation = true } else { store.clearHistory() } + }.disabled(store.results.isEmpty) + } + }.formStyle(.grouped).tabItem { Label("Reports & History", systemImage: "doc.richtext") } + Form { + Section("About PHPGuard") { + LabeledContent("Version", value: "1.9.0") + LabeledContent("Build", value: "10") + LabeledContent("Stored servers", value: "\(store.servers.count)") + Text("Server scans are read-only. Security advisories require independent verification.") + .font(.caption).foregroundStyle(.secondary) + } + }.formStyle(.grouped).tabItem { Label("About", systemImage: "info.circle") } + } + .frame(width: 680, height: 460) + .onAppear(perform: syncToStore) + .onChange(of: autoRefresh) { _, _ in syncToStore() } + .onChange(of: technical) { _, _ in syncToStore() } + .onChange(of: confirmClear) { _, _ in syncToStore() } + .onChange(of: accent) { _, _ in syncToStore() } + .onChange(of: historyLimit) { _, _ in syncToStore() } + .confirmationDialog("Delete all saved scan history?", isPresented: $showClearConfirmation) { + Button("Clear History", role: .destructive) { store.clearHistory() } + } message: { Text("This permanently removes saved scan results. Server configurations and cached CVEs are kept.") } + } + private func syncToStore() { + store.autoRefreshCVEs = autoRefresh + store.showTechnicalDetails = technical + store.confirmBeforeClearing = confirmClear + store.preferredReportAccent = accent + store.retainHistoryLimit = historyLimit + } +} diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md deleted file mode 100644 index 6ae08b8..0000000 --- a/RELEASE_NOTES.md +++ /dev/null @@ -1,10 +0,0 @@ -# PHPGuard 1.8.0 — Phase 9 - -- Removed the redundant floating Add Server toolbar button; Add Server remains in the sidebar and dashboard. -- NVD research records are cached in Application Support/PHPGuard/cve-cache.json. -- Cached CVEs load immediately when the application opens. -- NVD research refreshes automatically on application launch and can be refreshed manually. -- Network/API failures preserve cached CVE records and show an error. -- Existing app icon, installer artwork, server scans, report history and release scripts retained. - -CVE keyword results are research leads, not confirmed vulnerabilities for any scanned host. diff --git a/Scripts/release.sh b/Scripts/release.sh index 70465a2..11632de 100755 --- a/Scripts/release.sh +++ b/Scripts/release.sh @@ -6,10 +6,8 @@ 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; } -# Use the existing Apple notarization profile by default. -# Override with NOTARY_PROFILE when required. -PROFILE="${NOTARY_PROFILE:-orion-notary}" -echo "Notarization profile: $PROFILE" +PROFILE="${NOTARY_PROFILE:-}" +[[ -n "$PROFILE" ]] || { echo "Set NOTARY_PROFILE to an existing notarytool keychain profile." >&2; exit 1; } 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" diff --git a/Scripts/release.sh.backup-20261022-130947 b/Scripts/release.sh.backup-20261022-130947 deleted file mode 100755 index 11632de..0000000 --- a/Scripts/release.sh.backup-20261022-130947 +++ /dev/null @@ -1,36 +0,0 @@ -#!/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; } -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" -# 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" -xcrun stapler validate "$DMG" -echo "Release ready: $DMG" diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md new file mode 100644 index 0000000..871afa1 --- /dev/null +++ b/docs/RELEASE_NOTES.md @@ -0,0 +1,10 @@ +# PHPGuard 1.9.0 — Phase 10 + +- Redesigned report presentation with grouped cards and severity colours. +- Multi-page PDF report export with a coloured header, findings, inventory and footer. +- Clear all scan history with confirmation; delete individual scans. +- New native Settings window: Intelligence, Reports & History, About. +- Persistent preferences for CVE auto-refresh, history retention, PDF accent and confirmation. +- Fixed missing SettingsView.swift source reference in 1.8.0. + +Note: PDF reports are assessments, not verified CVE attestations.