diff --git a/docs/requirements/architecture.md b/docs/requirements/architecture.md index 908a349f7..2b0dac5ca 100644 --- a/docs/requirements/architecture.md +++ b/docs/requirements/architecture.md @@ -18,3 +18,23 @@ flowchart LR Note: diagram shows the feature-flag path only. SAST settings use two separate org-keyed caches (`orgToSastSettings` positive, 60s TTL; `orgToSastSettingsErr` negative, 60s TTL) that are also flushed on login. **Decision.** Feature flags are scoped to a Snyk organisation, not to individual workspace folders. Both the feature-flag and SAST settings caches therefore use the org ID as the cache key. Fetching on every call (no cache) was rejected first: with N folders each calling `PopulateFolderConfig` on `initialized`, an uncached design makes N×M HTTP calls per startup cycle. Per-folder caching was rejected next because it stores N redundant copies of the same org's data, multiplies HTTP calls when the cache is cold, and requires folder-level invalidation on auth changes. The feature-flag positive TTL is 30 seconds, satisfying the 60-second observation bound required by IDE-1898. Feature flags have no separate negative-error cache. Each flag is fetched concurrently in its own goroutine; if a goroutine encounters an error (401, timeout, server error), it stores `false` for that specific flag in the shared per-org result map, while the other goroutines proceed independently. Once all goroutines finish, the entire per-org map (all flags for that org) is written to the cache under the org key. There is therefore no per-flag cache entry — the cache key is always the org ID — but a fetch error only affects the individual flag(s) whose goroutine failed; flags whose goroutines succeeded retain their correct values. All flags are stored in the positive cache for the same 30-second TTL. The SAST settings positive TTL is 60 seconds; the SAST negative-error TTL (for 401/network failures) is also 60 seconds. All caches are flushed synchronously on re-authentication so that a fresh login observes updated values without waiting for any TTL to expire (satisfying IDE-1898 Req 3). + +## Auto-detect C/C++ workspaces delegated to the CLI OSS-flows extension + +- **Ticket:** IDE-2089 +- **Status:** Accepted + +```mermaid +flowchart LR + A["OSS Scan(folder)"] --> B["snyk-ls invokes\nsnyk test\n+ SNYK_AUTODETECT_OSS=1"] + B --> C["cli-extension-os-flows\nOSWorkflow"] + C --> D["managed scan\n(processAllInputDirectories)"] + C --> E{"detect.HasCPPArtefacts\nin input dirs"} + E -->|yes| F["invoke legacycli --unmanaged"] + E -->|no| G["return managed data"] + F --> H["append unmanaged data\nto managed output"] +``` + +**Decision.** Detection of C/C++ artefacts and the decision to run an unmanaged scan now live in the CLI's `cli-extension-os-flows` (`pkg/unmanaged/detect.HasCPPArtefacts`, orchestrated in `OSWorkflow` behind the `SNYK_AUTODETECT_OSS` env var). snyk-ls sets that env var unconditionally when invoking the OSS CLI; the extension scans each input directory and, if any C/C++ artefacts are present, runs an extra unmanaged scan via the legacy CLI alongside the managed scan and appends its `workflow.Data` to the managed output. The previous snyk-ls–side prompt, per-folder `snyk_oss_unmanaged_enabled` toggle, `snyk_oss_unmanaged_prompted` latch, `EnableUnmanagedScanCommand`, and the panel sub-toggle were all removed: with detection and routing centralised in the extension, no per-folder IDE state is needed and no user interaction is required. Two alternatives were rejected: (a) keeping detection in snyk-ls and only moving the decision would split the logic across two repos with no benefit; (b) requiring users to opt in via a feature flag was rejected because the env var is set by snyk-ls — end users see the new behaviour automatically — and the extension treats the gate as off-by-default for direct CLI invocations, preserving existing CLI behaviour for non-IDE users. + +**Limitation.** A native Go unmanaged workflow does not yet exist; the extension currently invokes the legacy TypeScript CLI to perform the unmanaged scan when C/C++ is detected. The legacy CLI's `workflow.Data` carries its own content type, so for mixed-content projects (manifest + C/C++) the unmanaged and managed results render as **separate sections** rather than as a single unified report. When a native `unmanaged.test` workflow is implemented, the extension can replace the legacy invocation and produce one merged structured output without changing snyk-ls behaviour. diff --git a/docs/requirements/requirements.md b/docs/requirements/requirements.md index 1cf7bf89a..80aac4f94 100644 --- a/docs/requirements/requirements.md +++ b/docs/requirements/requirements.md @@ -1,3 +1,6 @@ IDE-1898: The Eclipse plugin shall successfully complete LSP initialization regardless of the number of workspace folders. IDE-1898: When a feature flag is deactivated, the IDE plugin shall observe the change within 60 seconds. IDE-1898: When a user authenticates, the IDE plugin shall immediately re-evaluate feature flags without waiting for any previously cached authentication failures to expire. +IDE-2089: snyk-ls shall set `SNYK_AUTODETECT_OSS=1` in the environment of every Snyk OSS CLI invocation so the CLI's os-flows extension can decide per-folder whether to also run an unmanaged scan. +IDE-2089: The CLI's `cli-extension-os-flows` extension shall, when `SNYK_AUTODETECT_OSS` is truthy and `--unmanaged` was not explicitly passed, inspect each input directory for C/C++ source, header, or build-system files and run an extra unmanaged scan alongside the managed scan when any are found. +IDE-2089: When the extension runs an extra unmanaged scan it shall return the unmanaged results alongside the managed results so both are presented to the user without per-folder IDE configuration. diff --git a/infrastructure/oss/cli_scanner.go b/infrastructure/oss/cli_scanner.go index 7b615a29c..48984c16d 100644 --- a/infrastructure/oss/cli_scanner.go +++ b/infrastructure/oss/cli_scanner.go @@ -203,6 +203,7 @@ func (cliScanner *CLIScanner) Scan(ctx context.Context, pathToScan types.FilePat logger.Debug().Msg("Open Source scan skipped: path is not a supported manifest, lockfile, or directory") return []types.Issue{}, nil } + return cliScanner.scanInternal(ctx, cliScanner.prepareScanCommand) } @@ -392,6 +393,14 @@ func (cliScanner *CLIScanner) prepareScanCommand(args []string, parameterBlackli if params := cliScanner.configResolver.GetStringSlice(types.SettingCliAdditionalOssParameters, folderConfig); len(params) > 0 { args = append(args, params...) } + // Opt the CLI's os-flows extension into auto-detecting C/C++ projects: + // when set, it runs an extra unmanaged scan alongside the managed scan + // for folders that look unmanaged-eligible, so the LS doesn't need to + // prompt or expose a per-folder toggle. + if env == nil { + env = gotenv.Env{} + } + env["SNYK_AUTODETECT_OSS"] = "1" processedArgs := []string{} // now add all additional parameters, skipping blacklisted ones diff --git a/infrastructure/oss/cli_scanner_test.go b/infrastructure/oss/cli_scanner_test.go index a7821ffe7..94121f0f1 100644 --- a/infrastructure/oss/cli_scanner_test.go +++ b/infrastructure/oss/cli_scanner_test.go @@ -452,6 +452,36 @@ func TestConvertScanResultToIssues_IgnoredIssuesNotPropagated(t *testing.T) { assert.False(t, ignoredExists, "Expected the ignored issue to not be in the package issue cache") } +func TestCLIScanner_prepareScanCommand_SetsAutodetectEnv(t *testing.T) { + engine := testutil.UnitTest(t) + resolver := defaultResolver(t, engine) + cliScanner := &CLIScanner{ + engine: engine, + cli: cli.NewTestExecutorWithResponse(engine, "{}"), + instrumentor: performance.NewInstrumentor(), + errorReporter: error_reporting.NewTestErrorReporter(engine), + learnService: mock_learn.NewMockService(gomock.NewController(t)), + notifier: notification.NewMockNotifier(), + configResolver: resolver, + mutex: &sync.RWMutex{}, + inlineValueMutex: &sync.RWMutex{}, + packageScanMutex: &sync.Mutex{}, + runningScans: make(map[types.FilePath]*scans.ScanProgress), + supportedFiles: make(map[string]bool), + packageIssueCache: make(map[string][]types.Issue), + } + + folderPath := types.FilePath("/path/to/project") + fc := &types.FolderConfig{FolderPath: folderPath, ConfigResolver: resolver} + + _, env := cliScanner.prepareScanCommand([]string{}, map[string]bool{}, folderPath, fc) + + // The LS opts the CLI's os-flows extension into auto-detecting C/C++ projects; + // os-flows then runs an extra unmanaged scan when needed without the LS having + // to prompt or expose a per-folder toggle. + assert.Equal(t, "1", env["SNYK_AUTODETECT_OSS"]) +} + func Test_isForceLegacyCLI(t *testing.T) { t.Run("returns true when env var is set", func(t *testing.T) { t.Setenv("SNYK_FORCE_LEGACY_CLI", "1")