diff --git a/core/core/initutils.go b/core/core/initutils.go index 8a525e5d..f9348ab3 100644 --- a/core/core/initutils.go +++ b/core/core/initutils.go @@ -90,7 +90,11 @@ func getResourceHandler(ctx context.Context, scanInfo *cautils.ScanInfo, tenantC return resourcehandler.NewFileResourceHandler() } - getter.GetKSCloudAPIConnector() + // Only initialize cloud connector if not in air-gapped mode + // This call initializes the global cloud API connector for later use + if !isAirGappedMode(scanInfo) { + _ = getter.GetKSCloudAPIConnector() + } rbacObjects := getRBACHandler(tenantConfig, k8s, scanInfo.Submit) return resourcehandler.NewK8sResourceHandler(k8s, hostSensorHandler, rbacObjects, tenantConfig.GetContextName()) } diff --git a/core/core/scan.go b/core/core/scan.go index 156954c8..9275dfe2 100644 --- a/core/core/scan.go +++ b/core/core/scan.go @@ -66,9 +66,11 @@ func getInterfaces(ctx context.Context, scanInfo *cautils.ScanInfo) componentInt } // ================== version testing ====================================== - - v := versioncheck.NewIVersionCheckHandler(ctx) - _ = v.CheckLatestVersion(ctx, versioncheck.NewVersionCheckRequest(scanInfo.AccountID, versioncheck.BuildNumber, policyIdentifierIdentities(scanInfo.PolicyIdentifier), "", string(scanInfo.GetScanningContext()), k8sClient)) + // Skip version check in air-gapped mode (when keep-local flag is set) + if !scanInfo.Local { + v := versioncheck.NewIVersionCheckHandler(ctx) + _ = v.CheckLatestVersion(ctx, versioncheck.NewVersionCheckRequest(scanInfo.AccountID, versioncheck.BuildNumber, policyIdentifierIdentities(scanInfo.PolicyIdentifier), "", string(scanInfo.GetScanningContext()), k8sClient)) + } // ================== setup host scanner object ====================================== ctxHostScanner, spanHostScanner := otel.Tracer("").Start(ctx, "setup host scanner") @@ -132,7 +134,15 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH interfaces := getInterfaces(ctxInit, scanInfo) interfaces.report.SetTenantConfig(interfaces.tenantConfig) - downloadReleasedPolicy := getter.NewDownloadReleasedPolicy() // download config inputs from github release + // Only create DownloadReleasedPolicy if not in air-gapped mode + var downloadReleasedPolicy *getter.DownloadReleasedPolicy + if isAirGappedMode(scanInfo) { + // In air-gapped mode (--keep-local or using local files via --use-from, --controls-config, --exceptions, or attack tracks), + // don't initialize the downloader to prevent network access + downloadReleasedPolicy = nil + } else { + downloadReleasedPolicy = getter.NewDownloadReleasedPolicy() // download config inputs from github release + } // set policy getter only after setting the customerGUID scanInfo.Getters.PolicyGetter = getPolicyGetter(ctxInit, scanInfo.UseFrom, interfaces.tenantConfig.GetAccountID(), scanInfo.FrameworkScan, downloadReleasedPolicy) @@ -274,3 +284,13 @@ func scanSingleImage(ctx context.Context, img string, svc *imagescan.Service, re func isPrioritizationScanType(scanType cautils.ScanTypes) bool { return scanType == cautils.ScanTypeCluster || scanType == cautils.ScanTypeRepo } + +// isAirGappedMode returns true if the scan is configured to run in air-gapped mode +// (i.e., without any network access to download policies, exceptions, or other artifacts) +func isAirGappedMode(scanInfo *cautils.ScanInfo) bool { + return scanInfo.Local || + len(scanInfo.UseFrom) > 0 || + scanInfo.ControlsInputs != "" || + scanInfo.UseExceptions != "" || + scanInfo.AttackTracks != "" +} diff --git a/core/core/scan_test.go b/core/core/scan_test.go index fc9ee8b9..78406437 100644 --- a/core/core/scan_test.go +++ b/core/core/scan_test.go @@ -58,3 +58,66 @@ func TestIsPrioritizationScanType(t *testing.T) { }) } } + +func TestIsAirGappedMode(t *testing.T) { + tests := []struct { + name string + scanInfo *cautils.ScanInfo + want bool + }{ + { + name: "air-gapped with Local flag", + scanInfo: &cautils.ScanInfo{ + Local: true, + }, + want: true, + }, + { + name: "air-gapped with UseFrom", + scanInfo: &cautils.ScanInfo{ + UseFrom: []string{"/path/to/policy"}, + }, + want: true, + }, + { + name: "air-gapped with ControlsInputs", + scanInfo: &cautils.ScanInfo{ + ControlsInputs: "/path/to/controls", + }, + want: true, + }, + { + name: "air-gapped with UseExceptions", + scanInfo: &cautils.ScanInfo{ + UseExceptions: "/path/to/exceptions", + }, + want: true, + }, + { + name: "air-gapped with AttackTracks", + scanInfo: &cautils.ScanInfo{ + AttackTracks: "/path/to/attack-tracks", + }, + want: true, + }, + { + name: "not air-gapped - all empty", + scanInfo: &cautils.ScanInfo{}, + want: false, + }, + { + name: "air-gapped with multiple flags", + scanInfo: &cautils.ScanInfo{ + Local: true, + UseFrom: []string{"/path/to/policy"}, + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, isAirGappedMode(tt.scanInfo)) + }) + } +}