A typical .NET service pulls in hundreds of transitive NuGet packages, and the team wrote almost none of that code. Supply chain security interviews at the lead level exist because the biggest recent breaches — compromised build systems, hijacked maintainer accounts, typosquatted packages — didn't involve a bug in anyone's application code at all; they came in through a dependency or a pipeline the team trusted by default. Interviewers use this topic to check whether you think about a build pipeline as an asset that needs its own threat model, not just a means to an end. This page covers NuGet Audit and dependency scanning, lock files, package source mapping, SBOMs, package signing, SLSA, secrets that leak into repositories, and hardening CI/CD.
Q1 What makes a software supply chain attack different from a typical application vulnerability, and why does it deserve separate attention from a lead?#
Short answer: An application vulnerability is a flaw in code your team wrote and can fix directly; a supply chain attack compromises something you depend on but don't control — a package, a build tool, a CI runner, a maintainer's credentials — so the fix isn't a code review of your own logic, it's trust management across everything that touches your build and release process.
The reason this needs separate ownership at the lead level is that supply chain risk doesn't show up in the usual places: a thorough code review of your team's pull requests will never catch a backdoor injected into a transitive dependency's patch release, and traditional application security testing (SAST/DAST against your own code) doesn't scan the hundreds of packages your build pulls in from the internet on every run. Concretely, that means treating three things as first-class security surfaces that most teams treat as pure convenience: the dependency graph (what you depend on, transitively, and whether any of it has known vulnerabilities), the build pipeline itself (can an attacker who compromises a CI runner or a workflow file inject code into your release artifact), and the publishing/distribution step (can someone impersonate your package, or substitute a malicious one for a dependency you intended to pull from a trusted source). A lead's job is making these surfaces visible and owned — dependency scanning wired into CI, not run manually before a release; pipeline permissions reviewed like production access, not left at whatever GitHub's defaults happen to be.
What interviewers look for: distinguishing "a flaw in our code" from "a compromise of something we trust," and naming concrete surfaces (dependencies, build pipeline, distribution) rather than a vague "we should be more careful with dependencies."
Common mistakes: treating supply chain security as purely a dependency-scanning problem while leaving CI pipeline permissions and secrets management unreviewed.
Q2 Walk through how NuGet Audit works in a modern .NET project. What changed with .NET 10?#
Short answer: NuGet Audit checks your project's package references — direct and, depending on configuration, transitive — against a known-vulnerabilities database during restore, and surfaces warnings (NU1901 through NU1904, by increasing severity) without requiring any extra tooling; as of .NET 10, projects targeting net10.0 or later default to auditing the entire dependency graph rather than just direct references.
<PropertyGroup>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
</PropertyGroup>
<ItemGroup>
<!-- Suppress a specific advisory only after you've assessed it doesn't apply -->
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-xxxx-xxxx-xxxx" />
</ItemGroup>NuGetAuditMode controls scope (direct or all); before .NET 10 it defaulted to direct for every target framework, which meant a vulnerable transitive dependency two levels deep could sit unnoticed. NuGetAuditLevel sets the minimum severity worth a warning (low, moderate, high, critical), and NuGetAuditSuppress lets you formally acknowledge and exclude a specific advisory you've assessed as not applicable, rather than silencing all warnings globally. The data source matters operationally: audit relies on a server exposing a vulnerability-info resource, which nuget.org provides, so if your organization runs its own upstream feed without proxying that resource, you need a dedicated auditSources entry in nuget.config pointing at nuget.org's vulnerability-only endpoint, or audit silently has nothing to check against. Because this runs on every restore — which happens on every clean checkout, every CI build, every dotnet add package — it catches new advisories the moment they're published against packages you already depend on, not just at release time.
What interviewers look for: knowing the .NET 10 default-scope change specifically (a strong signal of staying current), and understanding that audit needs a working vulnerability data source to do anything at all.
Follow-up questions:
- Why is
direct-only auditing risky when most of a project's actual package count is transitive? - How would you make an audit warning fail the build in CI without breaking local development?
Q3 What are NuGet lock files, and what specific problem do they solve that Central Package Management does not?#
Short answer: A lock file (packages.lock.json, enabled per-project with RestorePackagesWithLockFile) pins the exact resolved version and content hash of every package — direct and transitive — so a restore on a different machine or a different day reproduces byte-for-byte the same dependency graph; Central Package Management pins versions centrally across projects, but without a lock file, a transitive dependency can still resolve to a different, newer patch release between two restores.
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>The distinction matters because CPM (Directory.Packages.props with ManagePackageVersionsCentrally) solves a consistency problem — every project in the solution uses the same version of a shared dependency — while lock files solve a reproducibility problem: without one, if PackageB (a transitive dependency you never directly reference) ships a new patch version between Monday's build and Tuesday's build, NuGet's version-range resolution can silently pick it up, and you've shipped a different dependency graph than the one you tested, with no code change on your side. The lock file's content hash also gives you tamper detection for free: if a package's content changes without its version changing — a compromised feed, a corrupted cache, a source that re-publishes under the same version — restore fails loudly instead of silently accepting different bytes under a version number you already trusted. The practical recommendation for a security-conscious team is both together: CPM for consistent, centrally-reviewed version choices, and lock files, committed to source control, for byte-for-byte reproducible restores in CI.
What interviewers look for: the reproducibility-versus-consistency distinction specifically, and recognizing that a lock file's hash check is a real tamper-detection mechanism, not just a version pin.
Common mistakes: assuming Central Package Management alone guarantees reproducible builds; not committing packages.lock.json to source control, which defeats the entire point.
Q4 Explain package source mapping. What attack does it directly prevent?#
Short answer: Package source mapping, configured in nuget.config, declares which package source is authoritative for which package ID patterns, so a package your project references can only ever be restored from the source you assigned it to — which directly prevents dependency confusion, where an attacker publishes a public package with the same name as your organization's private, internal-only package in the hope that a misconfigured build pulls the attacker's version from the public feed instead.
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="contoso-internal">
<package pattern="Contoso.*" />
</packageSource>
</packageSourceMapping>Without source mapping, a project configured with both a public and a private feed resolves each package ID against whichever source responds first or has the higher version — which is exactly the ambiguity dependency confusion exploits: publish Contoso.Internal.Auth to nuget.org at a higher version number than your real internal package, and some build configurations will happily pull the malicious public one. Source mapping removes that ambiguity entirely by making the source assignment explicit and pattern-based rather than resolved dynamically. It's worth being honest about the gap here for a lead-level answer: source mapping applies during restore, install, and update, but it doesn't currently apply to metadata-only commands like dotnet package add or dotnet list package --outdated, which still query every configured source for package metadata — so "just don't configure a public source" alongside a private one, or strict internal naming conventions that can't collide with anything realistically publishable, remain useful complementary defenses, not replaced by source mapping alone.
What interviewers look for: naming dependency confusion specifically as the attack this feature exists for, and being upfront about the current gap in metadata-command coverage rather than presenting source mapping as a complete solution.
Common mistakes: assuming a private feed alone is protection, without realizing NuGet will still check a public source configured alongside it unless mapping constrains that.
Q5 What is an SBOM, and what would you actually do with one if a critical CVE dropped in a widely-used library tomorrow?#
Short answer: A Software Bill of Materials is a structured, machine-readable inventory of every component in a build — direct and transitive dependencies, their exact versions, and typically their license and origin — expressed in a standard format like SPDX or CycloneDX; its entire value is answering "are we affected, and where, right now" in minutes instead of days when a new vulnerability is disclosed.
Without an SBOM, answering "do we use the affected library, and in which of our forty services" means grepping lockfiles and .csproj files across every repository, by hand, under time pressure — exactly the wrong moment to be doing manual discovery work. With SBOMs generated as part of every build (for example, via the Microsoft.Sbom.Targets MSBuild integration, which emits an SPDX-format document, or a CycloneDX generator for .NET producing the CycloneDX format), you can instead query a central inventory: which of our deployed artifacts include package X at version Y or earlier, ranked by how exposed each one is. That turns incident response from "find out if we're affected" into "confirm which of the affected artifacts we've already patched," which is a materially faster path to closing a zero-day. The operational discipline that makes this work is generating the SBOM at build time, attached to the actual release artifact, rather than trying to reconstruct one later from source — a reconstructed SBOM can drift from what was actually shipped, especially once floating version ranges or a mutated feed are in the picture.
What interviewers look for: an answer centered on incident response speed ("what do we do with it"), not just a definition — SBOMs are frequently asked about as a compliance checkbox, and a lead-level answer should go past that.
Common mistakes: treating SBOM generation as a one-time compliance artifact instead of something produced on every build and kept current.
Q6 How does NuGet package signing work, and what does it protect against versus what it doesn't?#
Short answer: NuGet supports two kinds of signatures — an author signature, applied by the package creator with their own registered certificate, which proves the package hasn't changed since that specific author signed it regardless of where it's later downloaded from; and a repository signature, which nuget.org applies automatically to every package uploaded to it (author-signed or not), guaranteeing package integrity specifically within that repository's distribution chain.
Signing protects against tampering in transit and at rest — a signed package whose bytes were altered after signing fails verification, whether the tampering happened on a compromised mirror, a corrupted cache, or a man-in-the-middle on a private feed. What it does not protect against is a legitimate maintainer's account being compromised and used to publish a genuinely, validly signed malicious update — that's an account-security and publishing-pipeline problem, not something a valid signature can detect, since the signature only proves "this is what the (possibly compromised) publisher actually uploaded." It's also worth knowing the practical limitation for a lead-level answer: author signing packages with dotnet nuget sign/nuget sign is currently a Windows-only workflow, which shapes how a cross-platform CI pipeline that needs to author-sign a release package has to be set up. Verification is the other half: dotnet nuget verify checks a package's signature chain against trusted certificates, and pairing that with client trust policies (accepting only packages signed by specific, known certificates) is what turns "this package is signed" into an actual access control, rather than just a tamper-evidence seal that nothing in your pipeline actually checks.
What interviewers look for: the tampering-versus-compromised-account distinction — it's the detail that shows you understand signing's real security boundary instead of treating "signed" as a synonym for "trustworthy."
Common mistakes: assuming a signed package can't be malicious; not configuring client trust policies, so signature verification happens but nothing actually enforces it.
Q7 Explain SLSA. What would it concretely mean to move a .NET build pipeline to a higher SLSA level?#
Short answer: SLSA (Supply-chain Levels for Software Artifacts) is a framework, now at v1.0, that defines increasing levels of build integrity for a software artifact's build track — from simply having a documented, scripted build process, up through generating signed, tamper-evident provenance from a build platform that's hardened against being manipulated by the build definition it's running.
In practice, moving up SLSA's build levels for a .NET pipeline means layering in verifiable evidence rather than just trusting that CI "did the right thing": the lower level is largely about having a consistent, scripted build (not a developer's laptop) with a documented process; the middle level adds authenticated, non-forgeable provenance — a signed statement of exactly what source commit, what build definition, and what dependencies produced a given artifact — generated by the build platform itself rather than something the build script could fabricate; the higher level adds isolation guarantees, where even someone who can modify the build definition can't tamper with the provenance-generation step, because it runs in an environment the build steps don't control. GitHub Actions supports this concretely today through the actions/attest-build-provenance action, which generates a signed SLSA-format provenance attestation for a build artifact using short-lived Sigstore certificates and publishes it to GitHub's attestations API, verifiable afterward with gh attestation verify — wiring that into a dotnet pack/dotnet nuget push release workflow is a concrete, achievable step a .NET team can take this quarter, not an abstract, multi-year compliance program.
What interviewers look for: treating SLSA as a practical, incremental framework you can point to concrete pipeline changes for, rather than reciting level numbers with no idea what changes between them.
Common mistakes: conflating "we use GitHub Actions" with "we have SLSA provenance," when provenance has to be explicitly generated and signed, not assumed from using a hosted CI provider.
Q8 A secret was committed to a Git repository and the commit was later removed. Walk through your incident response.#
Short answer: Removing the commit from the branch's current history does nothing for a secret that was ever pushed — it's still reachable through the reflog, forks, cached clones, CI logs, and any archive that ran before the removal — so the response starts with revoking and rotating the secret at its source, immediately, and only afterward deals with cleaning the repository's history.
Rotation first, history second, is the order that matters: rewriting history (with git filter-repo or BFG Repo Cleaner) to strip the secret from every commit is real work — it rewrites commit hashes, requires a force-push, and needs every collaborator to re-clone or hard-reset — and none of that undoes the fact that the original secret was already exposed to anyone who pulled, forked, or had CI access during the window it was live. Treat the exposure window as "from first push to the moment the credential is revoked," not "from first push to the moment the commit was deleted," because deletion alone changes nothing about what already left the repository. After rotation, the follow-up work is process, not just cleanup: figure out how it got committed in the first place (a missing .gitignore entry, a hardcoded connection string, a debug log left in) and add a preventive control — GitHub's secret scanning with push protection rejects a push containing a recognizable secret pattern before it ever lands in history, and a pre-commit hook running a scanner like gitleaks catches the same class of mistake locally, before it's even pushed.
What interviewers look for: the "rotate immediately, clean history second" ordering specifically — candidates who jump straight to git filter-repo without first revoking the credential are missing the actual point of the incident.
Common mistakes: treating history rewriting as sufficient remediation on its own; not checking CI/CD logs and build artifacts, which often echo secrets into places that outlive the commit itself.
Q9 How would you harden a GitHub Actions pipeline that builds and publishes a NuGet package, end to end?#
Short answer: Pin every third-party action to a full commit SHA rather than a mutable tag, scope the GITHUB_TOKEN to the minimum permissions the job actually needs, use OIDC federated credentials instead of a long-lived publishing secret wherever the target supports it, and require review on any change to the workflow file itself, since the workflow is as sensitive as the code it builds.
permissions:
contents: read
id-token: write # enables OIDC; no long-lived publish secret stored in the repo
jobs:
publish:
runs-on: ubuntu-latest
environment: production # requires a manual approval gate before this job runs
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2, pinned to a SHA
- run: dotnet pack -c Release
- run: dotnet nuget push "**/*.nupkg" --api-key "${{ steps.get-token.outputs.token }}"A tag like @v4 can be moved by whoever controls that action's repository, so pinning to a SHA is what actually guarantees the code that runs in your pipeline today is the code you reviewed, not whatever that tag points to tomorrow — this is precisely the class of attack that compromised a widely-used action in a real, publicly documented incident. Default GITHUB_TOKEN permissions are broader than most jobs need; declaring an explicit, minimal permissions block at the workflow or job level (read-only contents unless a step genuinely needs to write) limits what a compromised step or a malicious transitive action can do with that token. pull_request_target deserves specific caution: it runs with access to repository secrets even for PRs from forks, so checking out and running a fork's code under that trigger without care hands secrets to anyone who can open a PR. Finally, treat publishing credentials the same way you'd treat production database access — behind an environment with required reviewers, not a plain repository secret every workflow run can read.
What interviewers look for: naming SHA-pinning and least-privilege GITHUB_TOKEN scoping unprompted — both are cheap, concrete controls that a surprising number of production pipelines still skip.
Common mistakes: pinning actions to a version tag instead of a SHA; granting write-all permissions by default because it's the path of least resistance during initial setup.
Q10 How do you balance keeping dependencies current against the risk that an update itself introduces a compromise?#
Short answer: Automate the low-risk part (patch and minor version bumps for well-established, widely-used packages, via a tool like Dependabot or Renovate opening PRs your CI validates) and add friction deliberately to the high-risk part (major version jumps, brand-new or low-adoption packages, anything with maintainer or publishing changes), since the two failure modes — staying vulnerable by not updating, and pulling in a compromised update by updating blindly — need genuinely different mitigations, not one blanket policy.
The practical shape of this at lead level is a tiered update policy: security-only patch updates for direct and transitive dependencies flow through automatically once CI (build, tests, and NuGet Audit) passes, because the risk of staying on a known-vulnerable version usually exceeds the risk of a routine patch release; feature and major-version updates go through normal review, including an actual look at the changelog and, for anything unfamiliar, the maintainer and publish history. A few signals are worth specifically watching for on any dependency, new or existing: a sudden change in maintainership, a version jump that doesn't match the project's historical release cadence, install scripts or build-time code execution appearing in a package that never had any, and packages with very low download counts relative to how central they are to your build. None of this replaces the mechanical controls covered elsewhere on this page — audit, lock files, source mapping, signing — it's the human judgment layer on top of them, applied specifically at the moment a dependency graph is about to change, which is when supply chain compromises actually enter a codebase.
What interviewers look for: a risk-tiered policy (not "update everything immediately" or "pin everything forever") and concrete signals for evaluating an unfamiliar or newly-changed dependency, not just "we review pull requests."
Common mistakes: treating "always stay on the latest version" as an unqualified security best practice, ignoring that the update itself is an attack vector; freezing all dependencies indefinitely out of fear, which guarantees staying on known-vulnerable versions instead.
Quick-Fire Round#
| Question | Answer |
|---|---|
| What MSBuild property controls whether NuGet Audit checks transitive dependencies? | NuGetAuditMode, set to direct or all. |
| What changed about that property's default in .NET 10? | It defaults to all for projects targeting net10.0 or later, instead of direct. |
| What file enables reproducible, hash-verified restores? | packages.lock.json, via RestorePackagesWithLockFile. |
| What attack does package source mapping directly prevent? | Dependency confusion. |
| What two SBOM formats are most commonly produced for .NET builds? | SPDX and CycloneDX. |
| Who repository-signs every package uploaded to nuget.org? | nuget.org itself, automatically, regardless of author signing. |
| What GitHub Action generates a signed SLSA provenance attestation for a build artifact? | actions/attest-build-provenance. |
| What's the correct first step after finding a secret committed to a repo? | Revoke and rotate the secret immediately, before rewriting history. |
| Why is pinning a GitHub Action to a SHA safer than pinning to a tag? | A tag can be moved by the action's maintainer; a SHA can't. |
What permission should pull_request_target make you cautious about? | It exposes repository secrets even to workflow runs triggered by fork PRs. |
How to Prepare#
- Know the exact NuGet Audit MSBuild properties (
NuGetAuditMode,NuGetAuditLevel,NuGetAuditSuppress) and the .NET 10 default-scope change. - Be able to explain lock files as solving reproducibility (with tamper-evident hashes), distinct from Central Package Management's consistency guarantee.
- Practice the package source mapping answer with dependency confusion named explicitly as the attack it prevents, including its current metadata-command gap.
- Have one incident-response story ready for a leaked secret, in the correct order: rotate first, clean history second.
- Be ready to name concrete CI hardening controls — SHA-pinned actions, least-privilege
GITHUB_TOKEN, OIDC over long-lived secrets — not just "we use GitHub Actions securely." - Rehearse SLSA as a practical framework tied to a real action (
attest-build-provenance), not an abstract maturity model.