Skip to content
Open
20 changes: 20 additions & 0 deletions docs/requirements/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 3 additions & 0 deletions docs/requirements/requirements.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions infrastructure/oss/cli_scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] getCommand (infrastructure/cli/cli.go) treats a non-nil env as the complete subprocess environment and skips loading os.Environ(). Today that's safe because updateArgsGetEnvFromSystemAndConfiguration always returns a fully OS-populated map. But the new if env == nil { env = gotenv.Env{} } guard codifies a path that, if ever reached, would hand getCommand a map containing only SNYK_AUTODETECT_OSS=1 — stripping PATH and the rest of the OS env from the CLI subprocess. Consider a one-line comment naming that invariant, or setting the var via the existing AppendCliEnvironmentVariables merge path. Non-blocking.

— AI review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (non-blocking). The string literal "SNYK_AUTODETECT_OSS" + value "1" is the entire contract between snyk-ls and cli-extension-os-flows, which is the sole arbiter of whether an unmanaged C/C++ scan also runs. That boundary direction is correct (LS only opts in). Two small notes:

  • Consider anchoring this literal with a comment pointing at the extension's gate logic, mirroring the existing precedent for the hard-coded workflow ID in ostest_scan.go.
  • The if env == nil { env = gotenv.Env{} } guard just above is effectively dead in production — the SDK env path (UpdateEnvironmentAndReturnAdditionalParamsGetEnvFromSystemAndConfiguration) always returns a non-nil map; it only fires under test doubles. Harmless, keep or drop.

Verified the feared interactions are non-issues: setting this unconditionally alongside a user-supplied --unmanaged does not double-scan (--unmanaged forces the legacy path where this var is inert), and forcing env non-nil does not strip os.Environ()/PATH (env was already non-nil before this change).

— AI review


processedArgs := []string{}
// now add all additional parameters, skipping blacklisted ones
Expand Down
30 changes: 30 additions & 0 deletions infrastructure/oss/cli_scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading