Skip to content

feat(secrets): JIT Snyk Learn lessons for Secrets findings [EDU-4754]#1265

Open
SnykOleg wants to merge 25 commits into
mainfrom
edu-4754-secrets-learn-jit
Open

feat(secrets): JIT Snyk Learn lessons for Secrets findings [EDU-4754]#1265
SnykOleg wants to merge 25 commits into
mainfrom
edu-4754-secrets-learn-jit

Conversation

@SnykOleg

@SnykOleg SnykOleg commented May 10, 2026

Copy link
Copy Markdown

Why

Secrets findings in the IDE had no Snyk Learn JIT link on the issue card (unlike SAST/OSS). This adds CWE-798 lesson lookup during scan and product-aligned panel copy so IDE parity matches the web inventory work in EDU-4753 / app-ui#5425.

Jira: EDU-4754. Learn code actions (editor lightbulb) are out of scope.


Product review (Waleed / Jakob)

What you're reviewing: the Secrets issue details panel in the IDE (HTML from snyk-ls when you open a finding from the tree). Not the web inventory drawer (app-ui#5425 / #5426).

What changed for users

  • Secrets issue details can show a Snyk Learn link when the finding maps to CWE-798 (Hardcoded secrets lesson).
  • Copy: "Learn how to remediate Secrets securely" (secrets-specific; not the SAST string "Learn about this issue type").
  • Header uses Location: with a clickable line number.
  • Multiple locations in the same file show N LOCATIONS IN THIS FILE.
  • Existing Details and remediation accordion stays (static guidance text).

Previews: LS HTML with simulated VS Code Dark+ theme. Final spacing/fonts depend on the IDE webview.

Visuals

Scenario What to verify Preview
Learn link present (CWE-798) CTA text, external link icon, placement under header panel-with-learn
Learn link absent (no lesson resolved) No Learn block; rest of panel unchanged (no screenshot — panel shows Location: + Details accordion only; no Learn CTA between them)
Multi-location, same file Banner count matches locations in file panel-multi-location

IDE vs web (EDU-4753)

Element IDE (this PR) Web inventory
Learn JIT Issue card link Drawer (#5426)
Learn copy Learn how to remediate Secrets securely Align with #5426
Location Location: + line link Single Location row
Remediation body Collapsible in card How-to-Fix hidden (#5425)
Score / table sort N/A in card Web-only

Out of scope

  • Learn code actions (editor lightbulb) for secrets
  • IDE plugin changes (HTML panel only)
  • Web drawer score banner, table sort, single Location row

Product sign-off checklist

  • Learn CTA copy approved
  • Location: label matches web convention
  • Multi-location banner acceptable when 2+ hits in same file
  • No Learn link case acceptable (fail-soft, no error shown)

Optional live check

  1. Scan a repo with a CWE-798 secret (e.g. fake-leaks).
  2. Open the finding in the IDE tree → confirm panel shows Location + Learn link when lesson resolves.

Engineering details (for snyk-ls reviewers)

Summary

CWE-keyed lookup resolves the published "Hardcoded secrets" lesson for findings tagged with CWE-798 (first CWE only, same as SAST/OSS).

  1. Enable learn.Service.GetLesson for types.SecretsIssue in lessonsLookupParams.
  2. Populate Issue.LessonUrl in FindingsConverter.findingToIssues (single conversion pass).
  3. Render "Learn how to remediate Secrets securely" in the secrets details panel.
  4. Inject learn via secrets.Scanner.enrichContext so autosave / background scans get LessonUrl, not only workspace/executeCommand scans.
  5. Document SecretsIssue = 5 in README for snyk.getLearnLesson / snyk.openLearnLesson.
  6. Product alignment: Location: label; per-file LocationsCount for the multi-location banner.

Net diff vs main: infrastructure/learn/*, infrastructure/secrets/*, domain/ide/command/*_learn_lesson_test.go, application/di/init.go, README.md, docs/ui-rendering.md.

Fail-soft: Learn lookup errors, nil lessons, or missing learn service leave LessonUrl empty. Scans never fail because of Learn.

flowchart LR
  scan[secrets Scan] --> enrich[enrichContext]
  enrich --> convert[FindingsConverter.findingToIssues]
  convert --> lesson[learn.GetLesson CWE-798]
  lesson --> panel[details.html LessonUrl]
Loading

Companion work

  • Web (EDU-4753): app-ui#5426 (stacked on #5425). Parallel surface; not required to merge this ls PR.

Notes for the reviewer

  • @bastiandoetsch feedback addressed: Learn via request context; lesson lookup fused into FindingsConverter.findingToIssues.
  • SAST still uses struct field + post-scan enhancer; aligning SAST to this pattern is a separate concern.

Test plan

  • make lint — 0 issues
  • make test — pass (infrastructure/learn, infrastructure/secrets, domain/ide/command)
  • Learn lookup: lessonsLookupParams for SecretsIssueinfrastructure/learn/service_test.go
  • Conversion / scan / HTML: convert_test.go, secrets_test.go, secrets_html_test.go
  • Commands: getLearnLesson / openLearnLesson accept issueType=5
  • Docs: docs/ui-rendering.md (Secrets Issue Details Panel section)

Backwards compatibility

types.SecretsIssue ordinal is 5 (already on main). LS_PROTOCOL_VERSION unchanged.

Checklist

  • Tests added and all succeed
  • Regenerated mocks (make generate)
  • Linted (make lint)
  • README.md updated (SecretsIssue = 5)
  • Docs updated (docs/ui-rendering.md)

SnykOleg and others added 3 commits May 10, 2026 11:35
Add a new case in lessonsLookupParams for types.SecretsIssue so the
lookup returns a non-nil LessonLookupParams when invoked from the
getLearnLesson command (or from any future eager wiring).

Secrets findings carry no Learn-tagged rule (Phase 0b probe of
/v1/learn/lessons confirmed rules=[] on the CWE-798 'Hardcoded
secrets' lesson) and ship with empty Ecosystem (convert.go does not
set it). Pass an empty Rule and let the existing cache fall-through
(rule -> ecosystem -> all-lessons) plus filterForCWEs CWE intersection
resolve to the published CWE-798 lesson.

No wire-protocol change: the appended SecretsIssue ordinal (5) was
already in the iota block. LS_PROTOCOL_VERSION is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
… [EDU-4754]

Inject learn.Service into secrets.Scanner (mirroring how it is
injected into code.Scanner) and call a new enhanceWithLearnLesson
helper after the issue-aggregation loop in Scan and before the
cache mutations, matching the order in code.go:254-257.

The helper iterates the produced []types.Issue, calls
learnService.GetLesson with the issue's ecosystem, ID, CWEs, CVEs
and IssueType, and writes lesson.Url into the issue via
SetLessonUrl on success. Errors are logged and swallowed so a Learn
cache miss never blocks the scan.

Sentry instrumentation around the lookup is intentionally omitted:
secrets.Scanner has no errorReporter field today and the rest of
the package logs transient errors via its zerolog logger only.
Adding error_reporting.ErrorReporter just for one non-actionable
Learn-cache miss is disproportionate; revisit alongside any future
Sentry pass on the secrets package.

Application wiring updated in di/init.go to pass the existing
learnService into secrets.New.

Co-authored-by: Cursor <cursoragent@cursor.com>
When a secret issue carries a LessonUrl (set during scan by the
enhanceWithLearnLesson helper added in the previous commit), surface
it in the details panel as a 'Learn about this issue type' link --
identical markup to the code template at
infrastructure/code/template/details.html:66-74 so the two products
share the same UI affordance.

The required CSS rules (.sn-learn, .lesson-link.styled-link.is-external,
.lesson-icon, .is-external-icon) are already present in
infrastructure/secrets/template/styles.css; no stylesheet port is
needed. The template renderer is updated to pass the new data-map
keys (LessonUrl, LessonIcon, ExternalIcon).

Co-authored-by: Cursor <cursoragent@cursor.com>
@CLAassistant

CLAassistant commented May 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@snyk-io

snyk-io Bot commented May 10, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Comment thread infrastructure/secrets/secrets.go Outdated
@SnykOleg SnykOleg self-assigned this May 10, 2026
@SnykOleg SnykOleg marked this pull request as ready for review May 10, 2026 13:48
@SnykOleg SnykOleg requested review from a team as code owners May 10, 2026 13:48
@snyk-pr-review-bot

This comment has been minimized.

@SnykOleg

Copy link
Copy Markdown
Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (448e4c2)

Comment thread infrastructure/secrets/secrets.go Outdated
Comment thread infrastructure/secrets/secrets.go Outdated
@gitguardian

gitguardian Bot commented May 22, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@snyk-pr-review-bot

This comment has been minimized.

SnykOleg added 2 commits May 26, 2026 16:07
…U-4754]

Make learn.Service available to every JSON-RPC handler via the
request-context dependencies map, mirroring how authenticationService,
ldxSyncService, and inlineValueProvider are already plumbed through
withContext. Scanners can now consume the learn service from ctx
instead of carrying it as a struct field, satisfying the convention
in .cursor/rules/server-dependencies.mdc.

The DepLearnService key already existed in internal/context/context.go
and is read today by infrastructure/oss/unified_converter.go. Wiring
it through withContext means every scanner downstream of the LSP
handlers sees the same instance without per-scanner enrichContext
boilerplate.

Pre-existing pattern unchanged: dependencies are added to the deps map
only when non-nil, so tests that pass nil for unrelated dependencies
keep working.

Response to PR #1265 review comment from @bastiandoetsch:
#1265 (comment)
…earn lookup into FindingsConverter [EDU-4754]

Stop carrying learn.Service as a field on secrets.Scanner. The
production withContext middleware (server.go, previous commit) now
injects the service into every request context under DepLearnService;
the converter reads it back with the same lookup pattern used in
infrastructure/oss/unified_converter.go.

This change responds to two PR #1265 review comments from
@bastiandoetsch and aligns the secrets scanner with the convention in
.cursor/rules/server-dependencies.mdc.

1. Constructor surface (review comment 1)

secrets.Scanner.learnService field removed; secrets.New() reverts to
its pre-PR 8-parameter signature. application/di/init.go no longer
threads learnService through the secrets constructor. Production
wiring runs entirely through the request context.

2. Loop fusion (review comment 2)

The standalone Scanner.enhanceWithLearnLesson loop after the scan
aggregation is removed. The lesson lookup now lives inside
FindingsConverter.findingToIssues so each issue is enriched in the
single pass that converts it. ToIssues takes a context.Context and
extracts the learn.Service via learnServiceFromContext (local helper
mirroring oss/unified_converter.go:getLearnServiceAndErrorReporter).

A nil learnService, a lookup error, or a nil/empty lesson all leave
LessonUrl untouched - Learn cache misses must never fail the scan.

3. Tests

- All 16 secrets.New() call sites in secrets_test.go dropped the
  learnService argument. The 13 unrelated-to-Learn tests that
  previously used a no-op mock_learn stub now exercise the
  graceful-degradation path (nil service in ctx, no lookup, no
  LessonUrl - same behaviour the production code follows when the
  middleware has nothing to inject).
- The 3 lesson-specific tests (PopulatesLessonUrl,
  LessonUrlEmptyOnLearnError, LessonUrlEmptyOnNilLesson) inject the
  mock via a new withLearnServiceInContext test helper that mirrors
  the production withContext plumbing path.
- convert_test.go ToIssues calls take t.Context() to match the new
  signature.

4. Side-effect fix

This also unbreaks two pre-existing helpers in secrets_test.go
(seedScannerCache, scanWithEngineError) that were calling the old
8-arg New() signature and would not compile against the prior 9-arg
form. make lint is now 0 issues across the repo.

Review threads:
- #1265 (comment)
- #1265 (comment)
@snyk-pr-review-bot

This comment has been minimized.

@SnykOleg

SnykOleg commented Jun 8, 2026

Copy link
Copy Markdown
Author

HI @SnykOleg , looks like not all commits are signed. Would you please sign all commits (or squash & sign) and re-push?

@bastiandoetsch Done, re-signed 🙏

@SnykOleg SnykOleg requested a review from bastiandoetsch June 10, 2026 07:31
@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@SnykOleg

Copy link
Copy Markdown
Author

Hey @bastiandoetsch I hope this PR is good to go 🙏

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

cwes,
cves,
}
case types.SecretsIssue:

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.

Should fix — a Secrets finding with no CWE gets an arbitrary, unrelated lesson. This case passes an empty rule and relies on "CWE intersection through the all-lessons fall-through." That works only when the finding actually has a CWE. When it doesn't (extractProblems returns cwes=nil whenever there's no cwe problem), the lookup goes: empty rule + empty ecosystem → all-lessons branch → filterForCWEs with an empty CWE list, which filterLessonWithComparatorFunc treats as a no-op and returns the full list → GetLesson picks lessons[0]. So the "Learn how to remediate Secrets securely" link points at an arbitrary, unrelated lesson (and the selection is non-deterministic across runs, since GetAll() iterates a map).

Two reviewers flagged this independently. Suggested fix: in the Secrets path (or in enrichWithLearnLesson) skip the lookup / return no lesson when there is no CWE match, so a CWE-less secret gets no link rather than a wrong one. Add a real-service (non-mock) test asserting LessonUrl is empty for a no-CWE secret with a non-empty Learn cache — the current tests mock GetLesson and never exercise this path.

— AI review

AdditionalData: additionalData,
})
}
c.enrichWithLearnLesson(issue, learnService)

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 — redundant per-location Learn lookups. enrichWithLearnLesson is called inside the per-location loop, so a finding with N locations in a file emits N issues and issues N identical GetLesson calls (same ID/CWEs/CVEs/ecosystem). Since the Secrets path always misses the rule/ecosystem caches and falls through to a full lesson-catalog scan, each of those N calls scans the whole catalog. Resolve the lesson once per finding before the location loop and reuse the URL for every emitted issue.

Minor, non-blocking, related: learnServiceFromContext here duplicates the same DepLearnService context-read used in oss/unified_converter.go / oss/cli_scanner.go — worth a shared helper so the three copies can't drift.

— AI review

@basti-snyk

Copy link
Copy Markdown
Contributor

ℹ️ Automated AI review summary — non-blocking; actionable items are inline.

Reviewed origin/main...HEAD with four reviewers. Verdict: mostly solid, one should-fix correctness gap.

What's solid: HTML rendering is safe — {{.LessonUrl}} is interpolated in an href context via html/template (URL+HTML escaping; javascript: neutralized), icons come from trusted template.HTML internal helpers, and the link carries rel="noopener noreferrer". The cache-miss path is well-handled and tested (nil service / lookup error / empty lesson all leave LessonUrl empty, link gated behind {{if .LessonUrl}}). enrichContext correctly deep-copies the deps map before injecting the learn service, avoiding a shared-map race across concurrent scans. The converter signature change is fully propagated. No secret values are stored or rendered. No security findings introduced.

Should fix (see inline on service.go:334): A Secrets finding with no CWE gets an arbitrary, unrelated Learn lesson attached, because empty rule + empty ecosystem + empty CWE list falls through to lessons[0] (also non-deterministic across runs). Two reviewers flagged this independently; mock-based tests don't exercise it. Guard the no-CWE / no-match case to attach no lesson, and add a real-service test.

Suggestions:

  • Redundant per-location GetLesson lookups — hoist out of the location loop (inline on convert.go:161).
  • countSourceLocationsInFile is O(N²) per finding; precompute per-file counts once. Low impact (few locations in practice).
  • LocationsCount semantic change (total → per-file) is bundled into this PR; it's correct and tested but worth a PR-description note as a separate concern.
  • The SecretsIssue lookup-params comment says "ship with empty Ecosystem" but the code forwards the ecosystem arg; it works only because secrets always pass empty. Hard-code "" or fix the comment.

CI is red. Reviewers could not reproduce a failure in the touched packages locally (go build/vet/test green for infrastructure/secrets, infrastructure/learn, domain/ide/command; the sole secrets.New caller is updated, so it's not a compile break). Please read the actual failing job log before merge — likely a repo-wide lint/format/coverage/-race gate or an unrelated package, but it should be diagnosed, not assumed flaky.

— AI review

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants