diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
new file mode 100644
index 0000000..167f159
--- /dev/null
+++ b/.github/workflows/build-and-test.yml
@@ -0,0 +1,39 @@
+name: Package build & test
+
+on:
+ push:
+ branches: [ "main", "develop" ]
+ pull_request:
+ branches: [ "main", "develop" ]
+
+jobs:
+ build:
+ name: Build and Test SecurityToolkit scheme using any available iPhone simulator
+ runs-on: macos-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Set Default Scheme
+ run: |
+ default="SecurityToolkit"
+ echo $default | cat >default
+ echo Using default scheme: $default
+ - name: Build
+ env:
+ scheme: ${{ 'default' }}
+ platform: ${{ 'iOS Simulator' }}
+ run: |
+ # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
+ device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
+ if [ $scheme = default ]; then scheme=$(cat default); fi
+ xcodebuild build-for-testing -scheme "$scheme" -destination "platform=$platform,name=$device"
+ - name: Test
+ env:
+ scheme: ${{ 'default' }}
+ platform: ${{ 'iOS Simulator' }}
+ run: |
+ # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
+ device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
+ if [ $scheme = default ]; then scheme=$(cat default); fi
+ xcodebuild test -scheme "$scheme" -destination "platform=$platform,name=$device"
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SecurityToolkit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SecurityToolkit.xcscheme
new file mode 100644
index 0000000..320d94f
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SecurityToolkit.xcscheme
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SecurityToolkitExample.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SecurityToolkitExample.xcscheme
index 8a34f14..dc5c6cc 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/SecurityToolkitExample.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SecurityToolkitExample.xcscheme
@@ -1,7 +1,7 @@
+ version = "2.1">
+
+
+
+
+ shouldUseLaunchSchemeArgsEnv = "YES">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -35,7 +38,7 @@ Swift Package Manager
### SPM
`.package(url: "https://github.com/EXXETA/iOS-Security-Toolkit.git", from:
-"1.1.1")`
+"2.0.0")`
## CocoaPods
@@ -64,6 +67,10 @@ Use Async Stream API to get detected threats asynchronously:
- `ThreatDetectionCenter.threats: AsyncStream`
+## Testing
+
+To run the available tests locally or in the pipeline use `xcodebuild test -scheme SecurityToolkitTests -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.0'`
+
## Roadmap
Next features to be implemented:
diff --git a/SecurityToolkit.podspec b/SecurityToolkit.podspec
index ca95c83..dee58bf 100644
--- a/SecurityToolkit.podspec
+++ b/SecurityToolkit.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'SecurityToolkit'
- s.version = '1.1.1'
+ s.version = '2.0.0'
s.summary = 'Simple and easy security threat detector in Swift'
s.homepage = 'https://github.com/EXXETA/iOS-Security-Toolkit'
s.license = { :type => 'MIT', :file => 'LICENSE.md' }
diff --git a/SecurityToolkit.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SecurityToolkit.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 6b299ab..3bec300 100644
--- a/SecurityToolkit.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/SecurityToolkit.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,13 +1,13 @@
{
- "originHash" : "bb1fde65ac9d8228a88914af6416b18259fe2171aba8051e9d0785a832c7af82",
+ "originHash" : "3d50cc595c2a948bdc9828b418ac81066c7ea0247c8f72b21d19749ee60ed4e3",
"pins" : [
{
"identity" : "r.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mac-cain13/R.swift.git",
"state" : {
- "revision" : "384eab88d1a0b98ac96f4819e50a4308ecd5359f",
- "version" : "7.5.0"
+ "revision" : "a9abc6b0afe0fc4a5a71e1d7d2872143dff2d2f1",
+ "version" : "7.8.0"
}
},
{
@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tomlokhorst/XcodeEdit",
"state" : {
- "revision" : "b6b67389a0f1a6fdd9c6457a8ab5b02eaab13c5c",
- "version" : "2.9.2"
+ "revision" : "0e550cdee72844b35431afc3a1e176042be6d0f0",
+ "version" : "2.13.0"
}
}
],
diff --git a/SecurityToolkitExample/SecurityToolkitExample.xcodeproj/project.pbxproj b/SecurityToolkitExample/SecurityToolkitExample.xcodeproj/project.pbxproj
index 2556d5a..a4e21d2 100644
--- a/SecurityToolkitExample/SecurityToolkitExample.xcodeproj/project.pbxproj
+++ b/SecurityToolkitExample/SecurityToolkitExample.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 56;
+ objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -12,25 +12,44 @@
42B973FC2C57C4380040D9BC /* ThreatList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B973FB2C57C4380040D9BC /* ThreatList.swift */; };
42B973FE2C57C4390040D9BC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42B973FD2C57C4390040D9BC /* Assets.xcassets */; };
42C3DCE12C5A877B00973681 /* ThreatRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C3DCE02C5A877B00973681 /* ThreatRow.swift */; };
- 42C3DCE32C5A87B200973681 /* ThreatStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C3DCE22C5A87B200973681 /* ThreatStatus.swift */; };
+ 42C3DCE32C5A87B200973681 /* ThreatOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C3DCE22C5A87B200973681 /* ThreatOverview.swift */; };
42C3DCE52C5A892D00973681 /* Dimens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C3DCE42C5A892D00973681 /* Dimens.swift */; };
42C3DCE82C5CE54B00973681 /* RswiftLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 42C3DCE72C5CE54B00973681 /* RswiftLibrary */; };
42C3DCED2C5CE6F500973681 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 42C3DCEB2C5CE6F500973681 /* Localizable.strings */; };
42C3DCF52C5CEFE200973681 /* SystemImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C3DCF42C5CEFE200973681 /* SystemImage.swift */; };
+ 881894A52E71B35A00989BA6 /* SecurityToolkitExample.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 881894A42E71B35A00989BA6 /* SecurityToolkitExample.xctestplan */; };
+ 882CEB5F2D536FF0005D7171 /* ThreatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882CEB5D2D536FE8005D7171 /* ThreatViewModel.swift */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ 8818949E2E71B2AA00989BA6 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 42B973EE2C57C4380040D9BC /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 42B973F52C57C4380040D9BC;
+ remoteInfo = SecurityToolkitExample;
+ };
+/* End PBXContainerItemProxy section */
+
/* Begin PBXFileReference section */
42B973F62C57C4380040D9BC /* SecurityToolkitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecurityToolkitExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
42B973F92C57C4380040D9BC /* SecurityToolkitExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityToolkitExampleApp.swift; sourceTree = ""; };
42B973FB2C57C4380040D9BC /* ThreatList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreatList.swift; sourceTree = ""; };
42B973FD2C57C4390040D9BC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
42C3DCE02C5A877B00973681 /* ThreatRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreatRow.swift; sourceTree = ""; };
- 42C3DCE22C5A87B200973681 /* ThreatStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreatStatus.swift; sourceTree = ""; };
+ 42C3DCE22C5A87B200973681 /* ThreatOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreatOverview.swift; sourceTree = ""; };
42C3DCE42C5A892D00973681 /* Dimens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dimens.swift; sourceTree = ""; };
42C3DCEC2C5CE6F500973681 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
42C3DCF42C5CEFE200973681 /* SystemImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemImage.swift; sourceTree = ""; };
+ 8818949A2E71B2AA00989BA6 /* SecurityToolkitExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecurityToolkitExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 881894A42E71B35A00989BA6 /* SecurityToolkitExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SecurityToolkitExample.xctestplan; sourceTree = ""; };
+ 882CEB5D2D536FE8005D7171 /* ThreatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreatViewModel.swift; sourceTree = ""; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 8818949B2E71B2AA00989BA6 /* SecurityToolkitExampleTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SecurityToolkitExampleTests; sourceTree = ""; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
/* Begin PBXFrameworksBuildPhase section */
42B973F32C57C4380040D9BC /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -41,6 +60,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 881894972E71B2AA00989BA6 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -54,7 +80,9 @@
42B973ED2C57C4380040D9BC = {
isa = PBXGroup;
children = (
+ 881894A42E71B35A00989BA6 /* SecurityToolkitExample.xctestplan */,
42B973F82C57C4380040D9BC /* SecurityToolkitExample */,
+ 8818949B2E71B2AA00989BA6 /* SecurityToolkitExampleTests */,
42B973F72C57C4380040D9BC /* Products */,
424907A82C57C6A000BC1E7A /* Frameworks */,
);
@@ -64,6 +92,7 @@
isa = PBXGroup;
children = (
42B973F62C57C4380040D9BC /* SecurityToolkitExample.app */,
+ 8818949A2E71B2AA00989BA6 /* SecurityToolkitExampleTests.xctest */,
);
name = Products;
sourceTree = "";
@@ -89,10 +118,11 @@
42C3DCEA2C5CE64300973681 /* Sources */ = {
isa = PBXGroup;
children = (
+ 882CEB5D2D536FE8005D7171 /* ThreatViewModel.swift */,
42B973F92C57C4380040D9BC /* SecurityToolkitExampleApp.swift */,
42B973FB2C57C4380040D9BC /* ThreatList.swift */,
42C3DCE02C5A877B00973681 /* ThreatRow.swift */,
- 42C3DCE22C5A87B200973681 /* ThreatStatus.swift */,
+ 42C3DCE22C5A87B200973681 /* ThreatOverview.swift */,
42C3DCE42C5A892D00973681 /* Dimens.swift */,
42C3DCF42C5CEFE200973681 /* SystemImage.swift */,
);
@@ -124,6 +154,29 @@
productReference = 42B973F62C57C4380040D9BC /* SecurityToolkitExample.app */;
productType = "com.apple.product-type.application";
};
+ 881894992E71B2AA00989BA6 /* SecurityToolkitExampleTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 881894A02E71B2AA00989BA6 /* Build configuration list for PBXNativeTarget "SecurityToolkitExampleTests" */;
+ buildPhases = (
+ 881894962E71B2AA00989BA6 /* Sources */,
+ 881894972E71B2AA00989BA6 /* Frameworks */,
+ 881894982E71B2AA00989BA6 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 8818949F2E71B2AA00989BA6 /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ 8818949B2E71B2AA00989BA6 /* SecurityToolkitExampleTests */,
+ );
+ name = SecurityToolkitExampleTests;
+ packageProductDependencies = (
+ );
+ productName = SecurityToolkitExampleTests;
+ productReference = 8818949A2E71B2AA00989BA6 /* SecurityToolkitExampleTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -131,12 +184,16 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1540;
+ LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1540;
TargetAttributes = {
42B973F52C57C4380040D9BC = {
CreatedOnToolsVersion = 15.4;
};
+ 881894992E71B2AA00989BA6 = {
+ CreatedOnToolsVersion = 16.2;
+ TestTargetID = 42B973F52C57C4380040D9BC;
+ };
};
};
buildConfigurationList = 42B973F12C57C4380040D9BC /* Build configuration list for PBXProject "SecurityToolkitExample" */;
@@ -155,6 +212,7 @@
projectRoot = "";
targets = (
42B973F52C57C4380040D9BC /* SecurityToolkitExample */,
+ 881894992E71B2AA00989BA6 /* SecurityToolkitExampleTests */,
);
};
/* End PBXProject section */
@@ -169,6 +227,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 881894982E71B2AA00989BA6 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 881894A52E71B35A00989BA6 /* SecurityToolkitExample.xctestplan in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -178,10 +244,18 @@
files = (
42B973FC2C57C4380040D9BC /* ThreatList.swift in Sources */,
42C3DCF52C5CEFE200973681 /* SystemImage.swift in Sources */,
- 42C3DCE32C5A87B200973681 /* ThreatStatus.swift in Sources */,
+ 42C3DCE32C5A87B200973681 /* ThreatOverview.swift in Sources */,
42C3DCE12C5A877B00973681 /* ThreatRow.swift in Sources */,
42C3DCE52C5A892D00973681 /* Dimens.swift in Sources */,
42B973FA2C57C4380040D9BC /* SecurityToolkitExampleApp.swift in Sources */,
+ 882CEB5F2D536FF0005D7171 /* ThreatViewModel.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 881894962E71B2AA00989BA6 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -192,6 +266,11 @@
isa = PBXTargetDependency;
productRef = 42C3DCF02C5CEAD000973681 /* RswiftGenerateInternalResources */;
};
+ 8818949F2E71B2AA00989BA6 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 42B973F52C57C4380040D9BC /* SecurityToolkitExample */;
+ targetProxy = 8818949E2E71B2AA00989BA6 /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -344,7 +423,7 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.5;
@@ -396,6 +475,45 @@
};
name = Release;
};
+ 881894A12E71B2AA00989BA6 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.exxeta.SecurityToolkitExampleTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SecurityToolkitExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SecurityToolkitExample";
+ };
+ name = Debug;
+ };
+ 881894A22E71B2AA00989BA6 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.exxeta.SecurityToolkitExampleTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SecurityToolkitExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SecurityToolkitExample";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -417,6 +535,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 881894A02E71B2AA00989BA6 /* Build configuration list for PBXNativeTarget "SecurityToolkitExampleTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 881894A12E71B2AA00989BA6 /* Debug */,
+ 881894A22E71B2AA00989BA6 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
@@ -425,7 +552,7 @@
repositoryURL = "https://github.com/mac-cain13/R.swift.git";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 7.5.0;
+ minimumVersion = 7.8.0;
};
};
/* End XCRemoteSwiftPackageReference section */
diff --git a/SecurityToolkitExample/SecurityToolkitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SecurityToolkitExample/SecurityToolkitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index b8d583b..d65e458 100644
--- a/SecurityToolkitExample/SecurityToolkitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/SecurityToolkitExample/SecurityToolkitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/mac-cain13/R.swift.git",
"state" : {
- "revision" : "384eab88d1a0b98ac96f4819e50a4308ecd5359f",
- "version" : "7.5.0"
+ "revision" : "a9abc6b0afe0fc4a5a71e1d7d2872143dff2d2f1",
+ "version" : "7.8.0"
}
},
{
@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tomlokhorst/XcodeEdit",
"state" : {
- "revision" : "b6b67389a0f1a6fdd9c6457a8ab5b02eaab13c5c",
- "version" : "2.9.2"
+ "revision" : "0e550cdee72844b35431afc3a1e176042be6d0f0",
+ "version" : "2.13.0"
}
}
],
diff --git a/SecurityToolkitExample/SecurityToolkitExample.xctestplan b/SecurityToolkitExample/SecurityToolkitExample.xctestplan
new file mode 100644
index 0000000..64b5775
--- /dev/null
+++ b/SecurityToolkitExample/SecurityToolkitExample.xctestplan
@@ -0,0 +1,24 @@
+{
+ "configurations" : [
+ {
+ "id" : "2A48008B-945B-4FC4-B949-3BD5FEAAF6CF",
+ "name" : "Configuration 1",
+ "options" : {
+
+ }
+ }
+ ],
+ "defaultOptions" : {
+
+ },
+ "testTargets" : [
+ {
+ "target" : {
+ "containerPath" : "container:SecurityToolkitExample\/SecurityToolkitExample.xcodeproj",
+ "identifier" : "881894992E71B2AA00989BA6",
+ "name" : "SecurityToolkitExampleTests"
+ }
+ }
+ ],
+ "version" : 1
+}
diff --git a/SecurityToolkitExample/SecurityToolkitExample/Resources/en.lproj/Localizable.strings b/SecurityToolkitExample/SecurityToolkitExample/Resources/en.lproj/Localizable.strings
index 7fb6750..4920edf 100644
--- a/SecurityToolkitExample/SecurityToolkitExample/Resources/en.lproj/Localizable.strings
+++ b/SecurityToolkitExample/SecurityToolkitExample/Resources/en.lproj/Localizable.strings
@@ -17,5 +17,7 @@
"threat.list.description" = "Here is a list of the threats that could put you at risk";
// MARK: Threat Row
+"threat.row.state.initial" = "INITIAL";
"threat.row.state.detected" = "DETECTED";
"threat.row.state.safe" = "SAFE";
+"threat.row.state.exception" = "EXCEPTION";
diff --git a/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatList.swift b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatList.swift
index 2ed9457..f47ed0c 100644
--- a/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatList.swift
+++ b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatList.swift
@@ -1,11 +1,14 @@
import SwiftUI
+import SecurityToolkit
struct ThreatList: View {
+ @ObservedObject private var vm = ThreatViewModel.shared
+
var body: some View {
List {
Section {
- ForEach(ThreatStatus.threats, id: \.self) {
+ ForEach(vm.threatOverview, id: \.self) {
ThreatRow(threat: $0)
}
} header: {
diff --git a/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatOverview.swift b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatOverview.swift
new file mode 100644
index 0000000..264d576
--- /dev/null
+++ b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatOverview.swift
@@ -0,0 +1,48 @@
+import Foundation
+import SecurityToolkit
+
+struct ThreatOverview: Hashable {
+ let title: String
+ let description: String
+ let status: ThreatStatus
+ var isOk: Bool {
+ get { status == .notChecked || status == .notPresent }
+ }
+
+ static func threats(threatsReport: ThreatReport) -> [ThreatOverview] {
+ [
+ ThreatOverview(
+ title: R.string.localizable.threatJailbreakTitle(),
+ description: R.string.localizable.threatJailbreakDescription(),
+ status: threatsReport.rootPrivileges
+ ),
+ ThreatOverview(
+ title: R.string.localizable.threatHooksTitle(),
+ description: R.string.localizable.threatHooksDescription(),
+ status: threatsReport.hooks
+ ),
+ ThreatOverview(
+ title: R.string.localizable.threatSimulatorTitle(),
+ description: R.string.localizable.threatSimulatorDescription(),
+ status: threatsReport.simulator
+ ),
+ ThreatOverview(
+ title: R.string.localizable.threatDebuggerTitle(),
+ description: R.string.localizable.threatDebuggerDescription(),
+ status: threatsReport.debugger
+ ),
+ ThreatOverview(
+ title: R.string.localizable.threatPasscodeUnprotectedDeviceTitle(),
+ description: R.string.localizable.threatPasscodeUnprotectedDeviceDescription(),
+ status: threatsReport.devicePasscode
+ ),
+ ThreatOverview(
+ title: R.string.localizable.threatHardwareTitle(),
+ description: R.string.localizable.threatHardwareDescription(),
+ status: threatsReport.hardwareCryptography
+ ),
+ ]
+ }
+}
+
+
diff --git a/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatRow.swift b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatRow.swift
index d643ded..2edaf19 100644
--- a/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatRow.swift
+++ b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatRow.swift
@@ -2,21 +2,32 @@ import SwiftUI
struct ThreatRow: View {
- let threat: ThreatStatus
+ let threat: ThreatOverview
+
+ var threatText: String {
+ get {
+ switch threat.status {
+ case .notChecked: return R.string.localizable.threatRowStateInitial()
+ case .notPresent: return R.string.localizable.threatRowStateSafe()
+ case .present: return R.string.localizable.threatRowStateDetected()
+ case .exception: return R.string.localizable.threatRowStateException()
+ }
+ }
+ }
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(threat.title)
Spacer()
- Text(threat.isDetected ? R.string.localizable.threatRowStateDetected() : R.string.localizable.threatRowStateSafe())
+ Text(threatText)
.font(.caption2)
.foregroundColor(.white)
.padding(.horizontal, Dimens.unit8)
.padding(.vertical, Dimens.unit2)
.background {
RoundedRectangle(cornerRadius: Dimens.unit4)
- .fill(threat.isDetected ? .red : .green)
+ .fill(threat.isOk ? .green : .red)
}
}
Text(threat.description)
diff --git a/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatStatus.swift b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatStatus.swift
deleted file mode 100644
index 07e88b0..0000000
--- a/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatStatus.swift
+++ /dev/null
@@ -1,43 +0,0 @@
-import Foundation
-import SecurityToolkit
-
-struct ThreatStatus: Hashable {
- let title: String
- let description: String
- let isDetected: Bool
-
- static let threats = [
- ThreatStatus(
- title: R.string.localizable.threatJailbreakTitle(),
- description: R.string.localizable.threatJailbreakDescription(),
- isDetected: ThreatDetectionCenter.areRootPrivilegesDetected
- ),
- ThreatStatus(
- title: R.string.localizable.threatHooksTitle(),
- description: R.string.localizable.threatHooksDescription(),
- isDetected: ThreatDetectionCenter.areHooksDetected
- ),
- ThreatStatus(
- title: R.string.localizable.threatSimulatorTitle(),
- description: R.string.localizable.threatSimulatorDescription(),
- isDetected: ThreatDetectionCenter.isSimulatorDetected
- ),
- ThreatStatus(
- title: R.string.localizable.threatDebuggerTitle(),
- description: R.string.localizable.threatDebuggerDescription(),
- isDetected: ThreatDetectionCenter.isDebuggerDetected ?? false
- ),
- ThreatStatus(
- title: R.string.localizable.threatPasscodeUnprotectedDeviceTitle(),
- description: R.string.localizable.threatPasscodeUnprotectedDeviceDescription(),
- isDetected: ThreatDetectionCenter.isDeviceWithoutPasscodeDetected
- ),
- ThreatStatus(
- title: R.string.localizable.threatHardwareTitle(),
- description: R.string.localizable.threatHardwareDescription(),
- isDetected: ThreatDetectionCenter.isHardwareProtectionUnavailable
- ),
- ]
-}
-
-
diff --git a/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatViewModel.swift b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatViewModel.swift
new file mode 100644
index 0000000..ac32d5d
--- /dev/null
+++ b/SecurityToolkitExample/SecurityToolkitExample/Sources/ThreatViewModel.swift
@@ -0,0 +1,23 @@
+//
+// ThreatViewModel.swift
+// SecurityToolkitExample
+//
+// Created by Dobanda, Denis on 05.02.25.
+//
+import Foundation
+import SecurityToolkit
+import Combine
+
+class ThreatViewModel: ObservableObject {
+ static let shared: ThreatViewModel = ThreatViewModel()
+
+ @Published var threatOverview = [ThreatOverview]()
+
+ private init() {
+ ThreatDetectionCenter
+ .threatReports
+ .map(ThreatOverview.threats(threatsReport:))
+ .receive(on: DispatchQueue.main)
+ .assign(to: &$threatOverview)
+ }
+}
diff --git a/SecurityToolkitExample/SecurityToolkitExampleTests/SecurityToolkitExampleTests.swift b/SecurityToolkitExample/SecurityToolkitExampleTests/SecurityToolkitExampleTests.swift
new file mode 100644
index 0000000..ca45ce8
--- /dev/null
+++ b/SecurityToolkitExample/SecurityToolkitExampleTests/SecurityToolkitExampleTests.swift
@@ -0,0 +1,111 @@
+//
+// SecurityToolkitTests.swift
+// SecurityToolkitTests
+//
+// Created by Dobanda, Denis on 10.09.25.
+//
+
+import Testing
+import Combine
+import SecurityToolkit
+
+struct SecurityToolkitTests {
+
+ @Test func testDetectedThreadsForRootPrivileges() async throws {
+ var cancellable: Set = []
+ await confirmation { confirmation in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.rootPrivileges != .notChecked }
+ .sink { report in
+ #expect(report.rootPrivileges == .notPresent)
+ confirmation()
+ }
+ .store(in: &cancellable)
+ }
+ }
+
+ @Test func testDetectedThreadsForHooks() async throws {
+ var cancellable: Set = []
+ await confirmation { confirmation in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.hooks != .notChecked }
+ .sink { report in
+ #expect(report.hooks == .notPresent)
+ confirmation()
+ }
+ .store(in: &cancellable)
+ }
+ }
+
+ @Test func testDetectedThreadsForSimulator() async throws {
+ var cancellable: Set = []
+ await confirmation { confirmation in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.simulator != .notChecked }
+ .sink { report in
+ #expect(report.simulator == .present)
+ confirmation()
+ }
+ .store(in: &cancellable)
+ }
+ }
+
+ @Test func testDetectedThreadsForDebugger() async throws {
+ var cancellable: Set = []
+ await confirmation { confirmation in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.debugger != .notChecked }
+ .sink { report in
+ #expect(report.debugger == .present)
+ confirmation()
+ }
+ .store(in: &cancellable)
+ }
+ }
+
+ @Test func testDetectedThreadsForDevicePasscode() async throws {
+ var cancellable: Set = []
+ await confirmation { confirmation in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.devicePasscode != .notChecked }
+ .sink { report in
+ #expect(report.devicePasscode == .notPresent)
+ confirmation()
+ }
+ .store(in: &cancellable)
+ }
+ }
+
+ @Test func testDetectedThreadsForHardwareCryptography() async throws {
+ var cancellable: Set = []
+ await confirmation { confirmation in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.hardwareCryptography != .notChecked }
+ .sink { report in
+ #expect(report.hardwareCryptography == ThreatStatus.exception(ThreatDetectionException.checkNotPossible("Unexpected LAError: Biometry is not enrolled.")))
+ confirmation()
+ }
+ .store(in: &cancellable)
+ }
+ }
+
+
+ @Test func testConcurentDetectedThreads() async throws {
+ var cancellable: Set = []
+ await confirmation { c in
+ ThreatDetectionCenter.threatReports
+ .sink { _ in c()}
+ .store(in: &cancellable)
+ }
+
+ await confirmation { c in
+ ThreatDetectionCenter.threatReports
+ .sink { report in
+ #expect(report.rootPrivileges == .notPresent)
+ c()
+ }
+ .store(in: &cancellable)
+ }
+ }
+
+}
diff --git a/Sources/ThreatDetectionCenter.swift b/Sources/ThreatDetectionCenter.swift
index 499c7f8..fac4380 100644
--- a/Sources/ThreatDetectionCenter.swift
+++ b/Sources/ThreatDetectionCenter.swift
@@ -1,27 +1,112 @@
import Foundation
+import Combine
public final class ThreatDetectionCenter {
-
+
+ private init() {}
+
+ /// **Seconds** to wait for cycle recheck threads before running check again
+ /// > Should be positive and greater than 0, otherwise value is left unchanged
+ static public var threatReportsUpdateInterval: Int {
+ get { return _threatReportsUpdateInterval }
+ set {
+ if (newValue > 0) {
+ _threatReportsUpdateInterval = newValue
+ }
+ }
+ }
+
+
+ // MARK: - Async Threat Detection
+
+ // Private publisher for sending temperature updates
+ static private let reportPublisher = CurrentValueSubject(ThreatReport())
+
+ static private var task: Task<(), Never>?
+ static private var internalTasks: [Task<(), Never>] = []
+ static private var _threatReportsUpdateInterval = 10
+
+ /// Use this API to get ThreatReports
+ ///
+ /// > First access will start all Tasks and cache the result.
+ /// Next calls will not start any additional Tasks. Original result
+ /// will be cached and can be reused
+ static public var threatReports: AnyPublisher {
+ task = task ?? Task {
+ internalTasks.append(
+ Task {
+ repeat {
+ let status = JailbreakDetector.threatDetected()
+ reportPublisher.update { $0.copy(rootPrivileges: status) }
+ await insertDelay()
+ } while !Task.isCancelled
+ }
+ )
+ internalTasks.append(
+ Task {
+ repeat {
+ let status = HooksDetector.threatDetected()
+ reportPublisher.update { $0.copy(hooks: status) }
+ await insertDelay()
+ } while !Task.isCancelled
+ }
+ )
+ internalTasks.append(
+ Task {
+ let status = SimulatorDetector.threatDetected()
+ reportPublisher.update { $0.copy(simulator: status) }
+ }
+ )
+ internalTasks.append(
+ Task {
+ repeat {
+ let status = DebuggerDetector.threatDetected()
+ reportPublisher.update { $0.copy(debugger: status) }
+ await insertDelay()
+ } while !Task.isCancelled
+ }
+ )
+ internalTasks.append(
+ Task {
+ repeat {
+ let status = DevicePasscodeDetector.threatDetected()
+ reportPublisher.update { $0.copy(devicePasscode: status) }
+ await insertDelay()
+ } while !Task.isCancelled
+ }
+ )
+ internalTasks.append(
+ Task {
+ let status = HardwareSecurityDetector.threatDetected()
+ reportPublisher.update { $0.copy(hardwareCryptography: status) }
+ }
+ )
+ }
+ return reportPublisher.eraseToAnyPublisher()
+ }
+
+ public static func close() {
+ internalTasks.forEach { $0.cancel() }
+ task?.cancel()
+ internalTasks.removeAll()
+ task = nil
+ }
+
+ // MARK: - Sync API
+
/// Will check if jailbreak is present
///
- /// - Returns:
- /// `true`, if device is / was jailbroken;
- /// `false` otherwise
- ///
/// More about jailbreak: https://wikipedia.org/wiki/Jailbreak_%28iOS%29
///
/// > Should also detect jailbreak, even if the device is in a "safe" mode or
/// jailbreak mode is not active / was not properly removed
- public static var areRootPrivilegesDetected: Bool {
- JailbreakDetection.threatDetected()
+ @available(*, deprecated, message: "Migrate to use new threatReports async API")
+ public static var rootPrivilegesStatus: ThreatStatus {
+ JailbreakDetector.threatDetected()
}
-
+
/// Will check for an injection tool like Frida
///
- /// - Returns:
- /// `true`, if dynamic hooks are loaded at the time;
- /// `false` otherwise
- ///
/// More: https://fingerprint.com/blog/exploring-frida-dynamic-instrumentation-tool-kit/
///
/// > By the nature of dynamic hooks, this checks should be made on a regular
@@ -30,45 +115,34 @@ public final class ThreatDetectionCenter {
///
/// > Important: with a sufficient reverse engineering skills, this check can
/// be disabled. Use always in combination with another threats detections.
- public static var areHooksDetected: Bool {
- HooksDetection.threatDetected()
+ @available(*, deprecated, message: "Migrate to use new threatReports async API")
+ public static var hooksStatus: ThreatStatus {
+ HooksDetector.threatDetected()
}
-
+
/// Will check, if the app runs in a emulated / simulated environment
- ///
- /// - Returns:
- /// `true`, if simulator environment is detected;
- /// `false` otherwise
- public static var isSimulatorDetected: Bool {
- SimulatorDetection.threatDetected()
+ @available(*, deprecated, message: "Migrate to use new threatReports async API")
+ public static var simulatorStatus: ThreatStatus {
+ SimulatorDetector.threatDetected()
}
/// Will check, if the application is being traced by a debugger.
///
- /// - Returns:
- /// `true`, if a debugger is detected;
- /// `false`, if no debugger is detected;
- /// `nil`, if the detection process did not produce a definitive result.
- /// This could happen due to system limitations, lack of required
- /// permissions, or other undefined conditions.
- ///
/// A debugger is a tool that allows developers to inspect and modify the
/// execution of a program in real-time, potentially exposing sensitive data
/// or allowing unauthorized control.
///
/// > Please note that Apple itself may require a debugger for the app review
/// process.
- public static var isDebuggerDetected: Bool? {
- DebuggerDetection.threatDetected()
+ @available(*, deprecated, message: "Migrate to use new threatReports async API")
+ public static var debuggerStatus: ThreatStatus {
+ DebuggerDetector.threatDetected()
}
-
+
/// Will check, if current device is protected with at least a passcode
- ///
- /// - Returns:
- /// `true`, if device is unprotected;
- /// `false`, if device is protected with at least a passcode
- public static var isDeviceWithoutPasscodeDetected: Bool {
- DevicePasscodeDetection.threatDetected()
+ @available(*, deprecated, message: "Migrate to use new threatReports async API")
+ public static var devicePasscodeStatus: ThreatStatus {
+ DevicePasscodeDetector.threatDetected()
}
/// Will check, if current device has hardware protection layer
@@ -78,61 +152,37 @@ public final class ThreatDetectionCenter {
///
/// More: https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave
///
- /// - Returns:
- /// `true`, if device has no hardware protection;
- /// `false` otherwise
- ///
/// > Should be evaluated on a real device. Should only be used as an
/// indicator, if current device is capable of hardware protection. Does not
/// automatically mean, that encryption operations (keys, certificates,
/// keychain) are always backed by hardware. You should make sure, such
/// operations are implemented correctly with hardware layer
- public static var isHardwareProtectionUnavailable: Bool {
- HardwareSecurityDetection.threatDetected()
+ @available(*, deprecated, message: "Migrate to use new threatReports async API")
+ public static var hardwareCryptographyStatus: ThreatStatus {
+ HardwareSecurityDetector.threatDetected()
}
-
- // MARK: - Async Threat Detection
-
- /// Defines all possible threats, that can be reported via the stream
- public enum Threat: String {
- case rootPrivileges
- case hooks
- case simulator
- case debugger
- case deviceWithoutPasscode
- case hardwareProtectionUnavailable
+ // MARK: - Private API
+
+ static private func insertDelay() async {
+ try? await Task.sleep(nanoseconds: UInt64(_threatReportsUpdateInterval) * NSEC_PER_SEC)
}
-
- /// Stream that contains possible threats that could be detected
- public static var threats: AsyncStream {
- AsyncStream { continuation in
-
- if JailbreakDetection.threatDetected() {
- continuation.yield(.rootPrivileges)
- }
-
- if HooksDetection.threatDetected() {
- continuation.yield(.hooks)
- }
-
- if SimulatorDetection.threatDetected() {
- continuation.yield(.simulator)
- }
-
- if DebuggerDetection.threatDetected() ?? false {
- continuation.yield(.debugger)
- }
+}
- if DevicePasscodeDetection.threatDetected() {
- continuation.yield(.deviceWithoutPasscode)
+fileprivate extension CurrentValueSubject where Output: Equatable {
+ /// Use this function to update a value in the publisher atomically
+ func update(_ callback: (Output) -> Output) {
+ while true {
+ let value = self.value
+ let newValue = callback(value)
+ if (Task.isCancelled) {
+ return
+ } else if value == newValue {
+ return
+ } else if self.value == value {
+ self.value = newValue
+ return
}
-
- if HardwareSecurityDetection.threatDetected() {
- continuation.yield(.hardwareProtectionUnavailable)
- }
-
- continuation.finish()
}
}
}
diff --git a/Sources/ThreatReport.swift b/Sources/ThreatReport.swift
new file mode 100644
index 0000000..d76cca3
--- /dev/null
+++ b/Sources/ThreatReport.swift
@@ -0,0 +1,72 @@
+import Foundation
+
+/// An object describing latest detection status of every
+/// threat detectors
+public struct ThreatReport: Equatable, Hashable {
+ public let rootPrivileges: ThreatStatus
+ public let hooks: ThreatStatus
+ public let simulator: ThreatStatus
+ public let debugger: ThreatStatus
+ public let devicePasscode: ThreatStatus
+ public let hardwareCryptography: ThreatStatus
+
+ public init(
+ rootPrivileges: ThreatStatus = .notChecked,
+ hooks: ThreatStatus = .notChecked,
+ simulator: ThreatStatus = .notChecked,
+ debugger: ThreatStatus = .notChecked,
+ devicePasscode: ThreatStatus = .notChecked,
+ hardwareCryptography: ThreatStatus = .notChecked
+ ) {
+ self.rootPrivileges = rootPrivileges
+ self.hooks = hooks
+ self.simulator = simulator
+ self.debugger = debugger
+ self.devicePasscode = devicePasscode
+ self.hardwareCryptography = hardwareCryptography
+ }
+
+ func copy(
+ rootPrivileges: ThreatStatus? = nil,
+ hooks: ThreatStatus? = nil,
+ simulator: ThreatStatus? = nil,
+ debugger: ThreatStatus? = nil,
+ devicePasscode: ThreatStatus? = nil,
+ hardwareCryptography: ThreatStatus? = nil
+ ) -> ThreatReport {
+ return ThreatReport(
+ rootPrivileges: rootPrivileges ?? self.rootPrivileges,
+ hooks: hooks ?? self.hooks,
+ simulator: simulator ?? self.simulator,
+ debugger: debugger ?? self.debugger,
+ devicePasscode: devicePasscode ?? self.devicePasscode,
+ hardwareCryptography: hardwareCryptography ?? self.hardwareCryptography
+ )
+ }
+}
+
+public enum ThreatStatus: Equatable, Hashable {
+ case notChecked
+ case notPresent
+ case present
+ case exception(ThreatDetectionException)
+
+ public static func ==(lhs: ThreatStatus, rhs: ThreatStatus) -> Bool {
+ switch (lhs, rhs) {
+ case (.notChecked, .notChecked):
+ return true
+ case (.notPresent, .notPresent):
+ return true
+ case (.present, .present):
+ return true
+ case (.exception(let s1), .exception(let s2)):
+ return s1 == s2
+ default:
+ return false
+ }
+ }
+}
+
+public enum ThreatDetectionException: Error, Equatable, Hashable {
+ case checkNotPossible(String)
+}
diff --git a/Sources/internal/DebuggerDetection.swift b/Sources/internal/DebuggerDetection.swift
deleted file mode 100644
index 3cafcd2..0000000
--- a/Sources/internal/DebuggerDetection.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-import Foundation
-
-// MARK: - Internal
-internal final class DebuggerDetection {
-
- static func threatDetected() -> Bool? {
- hasTracerFlagSet()
- }
-}
-
-// MARK: - Private
-fileprivate extension DebuggerDetection {
-
- /// Check P_TRACED flag from Darwin Kernel
- /// if the process is traced
- private static func hasTracerFlagSet() -> Bool? {
- var info = kinfo_proc()
- // Kernel info, process info, specific process by PID, get current process ID
- var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
- var size = MemoryLayout.stride(ofValue: info)
-
- let unixStatusCode = sysctl(&mib, u_int(mib.count), &info, &size, nil, 0)
-
- return unixStatusCode == 0 ? (info.kp_proc.p_flag & P_TRACED) != 0 : nil
- }
-}
diff --git a/Sources/internal/DebuggerDetector.swift b/Sources/internal/DebuggerDetector.swift
new file mode 100644
index 0000000..ae67211
--- /dev/null
+++ b/Sources/internal/DebuggerDetector.swift
@@ -0,0 +1,37 @@
+import Foundation
+
+// MARK: - Internal
+internal final class DebuggerDetector {
+
+ static func threatDetected() -> ThreatStatus {
+ do {
+ let check = try hasTracerFlagSet()
+ return check ? .present : .notPresent
+ } catch let e {
+ let ex = e as? ThreatDetectionException
+ ?? ThreatDetectionException.checkNotPossible(e.localizedDescription)
+ return .exception(ex)
+ }
+ }
+}
+
+// MARK: - Private
+fileprivate extension DebuggerDetector {
+
+ /// Check P_TRACED flag from Darwin Kernel
+ /// if the process is traced
+ private static func hasTracerFlagSet() throws -> Bool {
+ var info = kinfo_proc()
+ // Kernel info, process info, specific process by PID, get current process ID
+ var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
+ var size = MemoryLayout.stride(ofValue: info)
+
+ let unixStatusCode = sysctl(&mib, u_int(mib.count), &info, &size, nil, 0)
+
+ if unixStatusCode != 0 {
+ throw ThreatDetectionException.checkNotPossible("Unexpected unix status code")
+ }
+
+ return (info.kp_proc.p_flag & P_TRACED) != 0
+ }
+}
diff --git a/Sources/internal/DevicePasscodeDetection.swift b/Sources/internal/DevicePasscodeDetection.swift
deleted file mode 100644
index ca796bd..0000000
--- a/Sources/internal/DevicePasscodeDetection.swift
+++ /dev/null
@@ -1,31 +0,0 @@
-import Foundation
-import LocalAuthentication
-
-// MARK: - Internal
-internal final class DevicePasscodeDetection {
-
- static func threatDetected() -> Bool {
- !hasDevicePasscode()
- }
-}
-
-// MARK: - Private
-fileprivate extension DevicePasscodeDetection {
-
- /// Will check if the user can perform authentication with a biometrics or
- /// a passcode
- private static func hasDevicePasscode() -> Bool {
- var error: NSError?
- let result = LAContext().canEvaluatePolicy(
- .deviceOwnerAuthentication,
- error: &error
- )
- if result {
- return true
- } else if let error {
- return error.code != LAError.passcodeNotSet.rawValue
- } else {
- return false
- }
- }
-}
diff --git a/Sources/internal/DevicePasscodeDetector.swift b/Sources/internal/DevicePasscodeDetector.swift
new file mode 100644
index 0000000..3482f1e
--- /dev/null
+++ b/Sources/internal/DevicePasscodeDetector.swift
@@ -0,0 +1,39 @@
+import Foundation
+import LocalAuthentication
+
+// MARK: - Internal
+internal final class DevicePasscodeDetector {
+
+ static func threatDetected() -> ThreatStatus {
+ hasDevicePasscode()
+ }
+}
+
+// MARK: - Private
+fileprivate extension DevicePasscodeDetector {
+
+ /// Will check if the user can perform authentication with a biometrics or
+ /// a passcode
+ private static func hasDevicePasscode() -> ThreatStatus {
+ var error: NSError?
+ let result = LAContext().canEvaluatePolicy(
+ .deviceOwnerAuthentication,
+ error: &error
+ )
+ if result {
+ return .notPresent
+ } else if let error {
+ if error.code != LAError.passcodeNotSet.rawValue {
+ return .exception(
+ ThreatDetectionException.checkNotPossible(
+ "Unexpected LAError: \(error.localizedDescription)"
+ )
+ )
+ } else {
+ return .present
+ }
+ } else {
+ return .present
+ }
+ }
+}
diff --git a/Sources/internal/HardwareSecurityDetection.swift b/Sources/internal/HardwareSecurityDetector.swift
similarity index 52%
rename from Sources/internal/HardwareSecurityDetection.swift
rename to Sources/internal/HardwareSecurityDetector.swift
index 2094417..9ba520e 100644
--- a/Sources/internal/HardwareSecurityDetection.swift
+++ b/Sources/internal/HardwareSecurityDetector.swift
@@ -2,18 +2,18 @@ import Foundation
import LocalAuthentication
// MARK: - Internal
-internal final class HardwareSecurityDetection {
+internal final class HardwareSecurityDetector {
- static func threatDetected() -> Bool {
- !isSecureEnclaveAvailable()
+ static func threatDetected() -> ThreatStatus {
+ isSecureEnclaveAvailable()
}
}
// MARK: - Private
-fileprivate extension HardwareSecurityDetection {
+fileprivate extension HardwareSecurityDetector {
/// See https://stackoverflow.com/a/49318485/7484013
- private static func isSecureEnclaveAvailable() -> Bool {
+ private static func isSecureEnclaveAvailable() -> ThreatStatus {
var error: NSError?
/// Policies can have certain requirements which, when not satisfied,
@@ -25,11 +25,19 @@ fileprivate extension HardwareSecurityDetection {
error: &error
)
if result {
- return true
+ return .notPresent
} else if let error {
- return error.code != LAError.biometryNotAvailable.rawValue
+ if error.code != LAError.biometryNotAvailable.rawValue {
+ return .exception(
+ ThreatDetectionException.checkNotPossible(
+ "Unexpected LAError: \(error.localizedDescription)"
+ )
+ )
+ } else {
+ return .present
+ }
} else {
- return false
+ return .present
}
}
}
diff --git a/Sources/internal/HooksDetection.swift b/Sources/internal/HooksDetector.swift
similarity index 88%
rename from Sources/internal/HooksDetection.swift
rename to Sources/internal/HooksDetector.swift
index bb4c394..157a21d 100644
--- a/Sources/internal/HooksDetection.swift
+++ b/Sources/internal/HooksDetector.swift
@@ -2,15 +2,16 @@ import Foundation
import MachO
// MARK: - Internal
-internal final class HooksDetection {
+internal final class HooksDetector {
- static func threatDetected() -> Bool {
- hasDynamicLibrariesLoaded() || hasSuspiciousFiles() || hasOpenPorts()
+ static func threatDetected() -> ThreatStatus {
+ let check = hasDynamicLibrariesLoaded() || hasSuspiciousFiles() || hasOpenPorts()
+ return check ? .present : .notPresent
}
}
// MARK: - Private
-fileprivate extension HooksDetection {
+fileprivate extension HooksDetector {
/// Check has loaded dynamic libraries
private static func hasDynamicLibrariesLoaded() -> Bool {
diff --git a/Sources/internal/JailbreakDetection.swift b/Sources/internal/JailbreakDetector.swift
similarity index 91%
rename from Sources/internal/JailbreakDetection.swift
rename to Sources/internal/JailbreakDetector.swift
index 97a18d5..425b1a9 100644
--- a/Sources/internal/JailbreakDetection.swift
+++ b/Sources/internal/JailbreakDetector.swift
@@ -2,15 +2,16 @@ import Foundation
import UIKit
// MARK: - Internal
-internal final class JailbreakDetection {
+internal final class JailbreakDetector {
- static func threatDetected() -> Bool {
- hasSuspiciousFiles() || hasUnexpectedFilePermissions() || canOpenSuspiciousLinks()
+ static func threatDetected() -> ThreatStatus {
+ let check = hasSuspiciousFiles() || hasUnexpectedFilePermissions() || canOpenSuspiciousLinks()
+ return check ? .present : .notPresent
}
}
// MARK: - Private
-fileprivate extension JailbreakDetection {
+fileprivate extension JailbreakDetector {
/// Check for suspicious files
static func hasSuspiciousFiles() -> Bool {
diff --git a/Sources/internal/SimulatorDetection.swift b/Sources/internal/SimulatorDetector.swift
similarity index 64%
rename from Sources/internal/SimulatorDetection.swift
rename to Sources/internal/SimulatorDetector.swift
index 951bd4e..a9701dc 100644
--- a/Sources/internal/SimulatorDetection.swift
+++ b/Sources/internal/SimulatorDetector.swift
@@ -1,15 +1,16 @@
import Foundation
// MARK: - Internal
-internal final class SimulatorDetection {
+internal final class SimulatorDetector {
- static func threatDetected() -> Bool {
- runsInSimulator()
+ static func threatDetected() -> ThreatStatus {
+ let check = runsInSimulator()
+ return check ? .present : .notPresent
}
}
// MARK: - Private
-fileprivate extension SimulatorDetection {
+fileprivate extension SimulatorDetector {
/// Check if the app is running in a simulator
static func runsInSimulator() -> Bool {
diff --git a/Tests/SecurityToolkitTests/SecurityToolkitTests.swift b/Tests/SecurityToolkitTests/SecurityToolkitTests.swift
new file mode 100644
index 0000000..2104976
--- /dev/null
+++ b/Tests/SecurityToolkitTests/SecurityToolkitTests.swift
@@ -0,0 +1,130 @@
+//
+// SecurityToolkitTests.swift
+// SecurityToolkitTests
+//
+// Created by Dobanda, Denis on 10.09.25.
+//
+
+import Foundation
+import Testing
+import Combine
+@testable import SecurityToolkit
+
+@Suite(.serialized) struct SecurityToolkitTests {
+
+ @Test func testNoDetectedThreadsAfterCancel() async throws {
+ var cancellable: Set = []
+ var closed = false
+ await withCheckedContinuation { cont in
+ ThreatDetectionCenter.threatReports
+ .sink { report in
+ if !closed {
+ ThreatDetectionCenter.close()
+ } else {
+ #expect(true == false, "Should never happen")
+ }
+ }
+ .store(in: &cancellable)
+
+ Task {
+ try? await Task.sleep(nanoseconds: UInt64(500) * NSEC_PER_MSEC)
+ cont.resume()
+ }
+ }
+ }
+
+ @Test func testDetectedThreadsForRootPrivileges() async throws {
+ var cancellable: Set = []
+ await withCheckedContinuation { cont in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.rootPrivileges != .notChecked }
+ .sink { report in
+ ThreatDetectionCenter.close()
+ #expect(report.rootPrivileges == .notPresent)
+ cont.resume()
+ }
+ .store(in: &cancellable)
+ }
+ try? await Task.sleep(nanoseconds: UInt64(100) * NSEC_PER_MSEC)
+ }
+
+ @Test func testDetectedThreadsForHooks() async throws {
+ var cancellable: Set = []
+ await withCheckedContinuation { cont in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.hooks != .notChecked }
+ .sink { report in
+ ThreatDetectionCenter.close()
+ // as the pipeline differs to local machine
+ #expect(report.hooks != .notChecked)
+ cont.resume()
+ }
+ .store(in: &cancellable)
+ }
+ try? await Task.sleep(nanoseconds: UInt64(100) * NSEC_PER_MSEC)
+ }
+
+ @Test func testDetectedThreadsForSimulator() async throws {
+ var cancellable: Set = []
+ await withCheckedContinuation { cont in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.simulator != .notChecked }
+ .sink { report in
+ ThreatDetectionCenter.close()
+ #expect(report.simulator == .present)
+ cont.resume()
+ }
+ .store(in: &cancellable)
+ }
+ try? await Task.sleep(nanoseconds: UInt64(100) * NSEC_PER_MSEC)
+ }
+
+ @Test func testDetectedThreadsForDebugger() async throws {
+ var cancellable: Set = []
+ await withCheckedContinuation { cont in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.debugger != .notChecked }
+ .sink { report in
+ ThreatDetectionCenter.close()
+ #expect(report.debugger == .notPresent)
+ cont.resume()
+ }
+ .store(in: &cancellable)
+ }
+ try? await Task.sleep(nanoseconds: UInt64(100) * NSEC_PER_MSEC)
+ }
+
+
+ @Test func testDetectedThreadsForDevicePasscode() async throws {
+ var cancellable: Set = []
+ await withCheckedContinuation { cont in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.devicePasscode != .notChecked }
+ .sink { report in
+ ThreatDetectionCenter.close()
+ #expect(report.devicePasscode == .notPresent)
+ cont.resume()
+ }
+ .store(in: &cancellable)
+ }
+ try? await Task.sleep(nanoseconds: UInt64(100) * NSEC_PER_MSEC)
+ }
+
+ @Test func testDetectedThreadsForHardwareCryptography() async throws {
+ var cancellable: Set = []
+ await withCheckedContinuation { cont in
+ ThreatDetectionCenter.threatReports
+ .filter{ $0.hardwareCryptography != .notChecked }
+ .sink { report in
+ ThreatDetectionCenter.close()
+ switch (report.hardwareCryptography) {
+ case .exception(_): _ = report
+ default: #expect(report.hardwareCryptography == ThreatStatus.exception(ThreatDetectionException.checkNotPossible("")))
+ }
+ cont.resume()
+ }
+ .store(in: &cancellable)
+ }
+ try? await Task.sleep(nanoseconds: UInt64(100) * NSEC_PER_MSEC)
+ }
+}