DEV Community

Cover image for Lambda SBOM: Which function ships which npm package?
Sascha Mirow-Stanitzok
Sascha Mirow-Stanitzok

Posted on Originally published at sascha-ms.de

Lambda SBOM: Which function ships which npm package?

A Lambda SBOM answers the exact question that comes first in every supply chain incident: which of our functions ship this package in this version? If you only start searching repositories and comparing lockfiles at that point, you lose hours. And in the end, nobody knows for sure what is actually deployed.

This topic is on my mind because serverless projects get hard to overview quickly. Twenty, fifty, a hundred small functions, each with its own bundle. Since November 6, 2026, AWS Security Hub shows an SBOM inventory across all accounts, fed by Amazon Inspector. That is a good reason to take a closer look. Bundled TypeScript functions in particular come with a catch, and the best place to fix it is the build.

In short

An SBOM lists every package and version inside a Lambda function and turns “where do we use package X?” into a question you can answer in seconds. Security Hub now provides a ready-made inventory based on Inspector scans. The biggest pitfall: functions bundled with esbuild lack the manifest files, so you should also generate the SBOM yourself from the esbuild metafile.

What you will learn in this article

  • what an SBOM is and which formats (CycloneDX, SPDX) exist
  • how Inspector and Security Hub build the SBOM inventory for Lambda
  • why bundled TypeScript functions can leave gaps in the inventory
  • how to create one SBOM per function with AWS CDK and a small script
  • how to turn that into a pipeline gate and an incident search

What is an SBOM and why is it worth it for Lambda?

SBOM stands for software bill of materials, a parts list for software. It names every component inside an artifact: name, version, often license and origin as well. The idea comes from manufacturing. When a part is faulty, you check the parts list and immediately know which products are affected.

For software, two machine-readable formats have won: CycloneDX (by OWASP) and SPDX (by the Linux Foundation). Both support JSON, and both know the package URL, or purl for short. A purl such as pkg:npm/zod@4.6.7 identifies a package uniquely across ecosystems. It is the key that lets scanners, databases and scripts speak the same language.

With Lambda, the benefit is especially large. Every function is its own deployment artifact with its own dependency state. Two functions from the same repository can contain different versions if one has not been redeployed for a while. A look at the current lockfile therefore tells you what would be inside on the next deploy. Only the SBOM of the deployed artifact tells you what is running right now.

What AWS now provides: the SBOM inventory in Security Hub

Amazon Inspector has been scanning Lambda functions for vulnerable packages for a while and creates an SBOM per resource along the way. What is new: Security Hub collects these SBOMs and shows them as a searchable inventory. In the console you find it under Inventory → SBOM: one row per package and version, plus the number of resources it is installed on. Clicking the package lists the affected functions, instances and container images.

The key facts from the documentation and the announcement:

  • Covered are EC2 instances, ECR images and Lambda functions, plus connected Azure resources.
  • There is nothing to set up in Security Hub. The prerequisite is that Inspector is active for the resource type, for Lambda that means Lambda standard scanning.
  • After a new scan, it can take a few hours until the change shows up in the inventory.
  • Administrator accounts see the whole organization, member accounts only their own resources.
  • According to AWS, the inventory is included in Security Hub Essentials at no extra cost. Inspector itself is not free: Lambda standard scanning costs USD 0.30 per function per month in us-east-1, and new accounts get a 15-day trial.

Two limitations from the Inspector documentation are worth knowing. Only functions that were invoked or changed within the past 90 days get scanned, and only their $LATEST version. In addition, Inspector does not check the AWS SDK that ships with the Lambda runtime, only dependencies from your code package and from layers.

The catch with bundled TypeScript functions

This is where it gets interesting for TypeScript teams. A package scanner recognizes npm dependencies by their manifest files. For JavaScript, the documentation of the Inspector SBOM Generator lists, for example, node_modules/*/package.json, package-lock.json, pnpm-lock.yaml and yarn.lock. With a bundled deployment, exactly these files do not end up in the ZIP.

When you bundle with esbuild (as NodejsFunction in CDK or the built-in build of Serverless Framework v4 does), you get a single file index.mjs. All packages are melted into it, and package names and version numbers no longer exist as files. That is great for cold starts, but bad for any scanner looking for manifests.

Comparison: Lambda ZIP without bundling, with package.json files, versus an esbuild bundle with a single index.mjs

Without manifests the scanner lacks package information. The way out is an SBOM from the build.

I want to phrase this carefully: AWS does not describe in detail how well Inspector still detects packages inside a minified bundle. So my advice: deploy a bundled test function with a known dependency, wait for a scan and check the inventory. If the package is missing, you have your answer. Either way, an SBOM from the build is the more precise source, because esbuild knows exactly which files from which package ended up in the bundle.

Creating a Lambda SBOM yourself: step by step with AWS CDK

The idea is simple. While bundling, esbuild can write a metafile, a JSON file listing every input file in the bundle together with its byte count. The paths under node_modules/ reveal the packages, and the matching package.json provides the version. From that, you build one CycloneDX file per function. The example is a fictional bike rental with two functions: RentBikeFn uses zod and ulid, ListStationsFn only zod.

🎬 Flow: creating a Lambda SBOM at build time – this animated diagram only runs on my site: watch it on sascha-ms.de

Prerequisites

  • Node.js 24 (locally and as the Lambda runtime)
  • a CDK project with aws-cdk-lib 2.275 and CDK CLI 2.1148 (tested)
  • esbuild as a local dev dependency, so CDK bundles without Docker
  • for part two: AWS CLI v2 and permissions for Inspector and Security Hub

Step 1: Project and dependencies

Set up the dependencies. Having esbuild in devDependencies matters: without it, CDK bundles inside a Docker container, and the paths in the metafile look different.

// package.json (excerpt)
{
  "type": "module",
  "scripts": {
    "synth": "cdk synth --quiet",
    "sbom": "tsx scripts/sbom-from-cdk-out.ts",
    "find": "tsx scripts/find-package.ts"
  },
  "dependencies": { "ulid": "3.0.2", "zod": "4.6.7" },
  "devDependencies": {
    "@aws-sdk/client-securityhub": "^3.1164.0",
    "@types/aws-lambda": "^8.10.160",
    "@types/node": "^24.0.0",
    "aws-cdk": "2.1148.0",
    "aws-cdk-lib": "2.275.0",
    "constructs": "^10.4.2",
    "esbuild": "0.28.2",
    "tsx": "^4.20.0",
    "typescript": "^5.9.0"
  }
}
Enter fullscreen mode Exit fullscreen mode
npm install
Enter fullscreen mode Exit fullscreen mode

Step 2: Enable the metafile during bundling

NodejsFunction has a metafile option. When enabled, CDK places an index.meta.json next to index.mjs in the asset folder under cdk.out.

// lib/bike-rental-stack.ts
import { Stack, type StackProps, Tags } from "aws-cdk-lib";
import { Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction, OutputFormat } from "aws-cdk-lib/aws-lambda-nodejs";
import type { Construct } from "constructs";

export class BikeRentalStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Same bundling settings for all functions.
    // metafile: true writes index.meta.json next to the bundle –
    // we build the SBOM from it later.
    const bundling = {
      format: OutputFormat.ESM,
      minify: true,
      sourceMap: false,
      metafile: true,
    };

    new NodejsFunction(this, "RentBikeFn", {
      entry: "src/rent-bike.ts",
      runtime: Runtime.NODEJS_24_X,
      bundling,
    });

    new NodejsFunction(this, "ListStationsFn", {
      entry: "src/list-stations.ts",
      runtime: Runtime.NODEJS_24_X,
      bundling,
    });

    Tags.of(this).add("app", "bike-rental");
  }
}
Enter fullscreen mode Exit fullscreen mode
// bin/app.ts
import { App } from "aws-cdk-lib";
import { BikeRentalStack } from "../lib/bike-rental-stack.js";

const app = new App();
new BikeRentalStack(app, "BikeRentalStack");
Enter fullscreen mode Exit fullscreen mode

One of the two functions looks like this, the second one follows the same pattern:

// src/rent-bike.ts
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { ulid } from "ulid";
import { z } from "zod";

const RentRequest = z.object({
  bikeId: z.string().min(3),
  stationId: z.string().min(3),
});

export const handler = async (
  event: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyResultV2> => {
  const parsed = RentRequest.safeParse(JSON.parse(event.body ?? "{}"));
  if (!parsed.success) {
    return { statusCode: 400, body: JSON.stringify({ error: "Invalid request" }) };
  }
  const rentalId = ulid();
  return {
    statusCode: 201,
    body: JSON.stringify({ rentalId, ...parsed.data }),
  };
};
Enter fullscreen mode Exit fullscreen mode

Step 3: The SBOM script

The script reads all templates in cdk.out, looks for resources of type AWS::Lambda::Function and follows their aws:asset:path metadata entry to the asset folder. There it evaluates the metafile. It only takes inputs with bytesInOutput greater than zero. Anything esbuild removed entirely through tree shaking therefore does not end up in the SBOM, which is exactly what you want.

// scripts/sbom-from-cdk-out.ts
// Creates one CycloneDX SBOM per Lambda function from the esbuild metafile
// that CDK writes to cdk.out during bundling (metafile: true).
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { randomUUID } from "node:crypto";

const CDK_OUT = process.argv[2] ?? "cdk.out";
const SBOM_DIR = process.argv[3] ?? "sbom";

type Metafile = {
  outputs: Record<string, { inputs: Record<string, { bytesInOutput: number }> }>;
};
type Pkg = { name: string; version: string; dir: string };

// "node_modules/@scope/name/x.js" or ".../node_modules/a/node_modules/b/y.js"
// → package folder of the innermost node_modules entry
export function packageDirFromInput(input: string): string | undefined {
  const marker = "node_modules/";
  const idx = input.lastIndexOf(marker);
  if (idx === -1) return undefined;
  const rest = input.slice(idx + marker.length).split("/");
  const nameParts = rest[0].startsWith("@") ? rest.slice(0, 2) : rest.slice(0, 1);
  return input.slice(0, idx + marker.length) + nameParts.join("/");
}

function readPackage(dir: string): Pkg | undefined {
  const file = join(dir, "package.json");
  if (!existsSync(file)) return undefined;
  const json = JSON.parse(readFileSync(file, "utf8")) as { name?: string; version?: string };
  if (!json.name || !json.version) return undefined;
  return { name: json.name, version: json.version, dir };
}

export function purl(name: string, version: string): string {
  const encoded = name.startsWith("@") ? "%40" + name.slice(1) : name;
  return `pkg:npm/${encoded}@${version}`;
}

function packagesFromMetafile(meta: Metafile): Pkg[] {
  const found = new Map<string, Pkg>();
  for (const output of Object.values(meta.outputs)) {
    for (const [input, info] of Object.entries(output.inputs)) {
      if (info.bytesInOutput === 0) continue; // fully tree-shaken away
      const dir = packageDirFromInput(input);
      if (!dir || found.has(dir)) continue;
      const pkg = readPackage(dir);
      if (pkg) found.set(dir, pkg);
    }
  }
  // Count the same version from different folders only once
  const unique = new Map<string, Pkg>();
  for (const p of found.values()) unique.set(`${p.name}@${p.version}`, p);
  return [...unique.values()].sort((a, b) => a.name.localeCompare(b.name));
}

type Template = {
  Resources: Record<string, { Type: string; Metadata?: Record<string, unknown> }>;
};

mkdirSync(SBOM_DIR, { recursive: true });
let count = 0;

for (const file of readdirSync(CDK_OUT).filter((f) => f.endsWith(".template.json"))) {
  const stackName = file.replace(".template.json", "");
  const template = JSON.parse(readFileSync(join(CDK_OUT, file), "utf8")) as Template;

  for (const [logicalId, resource] of Object.entries(template.Resources)) {
    if (resource.Type !== "AWS::Lambda::Function") continue;
    const assetPath = resource.Metadata?.["aws:asset:path"];
    const cdkPath = String(resource.Metadata?.["aws:cdk:path"] ?? logicalId);
    if (typeof assetPath !== "string") continue;

    const metaFile = join(CDK_OUT, assetPath, "index.meta.json");
    if (!existsSync(metaFile)) {
      console.warn(`⚠ ${cdkPath}: no index.meta.json – is bundling.metafile enabled?`);
      continue;
    }

    const meta = JSON.parse(readFileSync(metaFile, "utf8")) as Metafile;
    const packages = packagesFromMetafile(meta);

    const sbom = {
      bomFormat: "CycloneDX",
      specVersion: "1.5",
      serialNumber: `urn:uuid:${randomUUID()}`,
      version: 1,
      metadata: {
        timestamp: new Date().toISOString(),
        component: {
          type: "application",
          name: cdkPath.replace(/\/Resource$/, ""),
          "bom-ref": `${stackName}/${logicalId}`,
          properties: [
            { name: "cdk:stack", value: stackName },
            { name: "cdk:logicalId", value: logicalId },
            { name: "cdk:asset", value: assetPath },
          ],
        },
      },
      components: packages.map((p) => ({
        type: "library",
        name: p.name,
        version: p.version,
        purl: purl(p.name, p.version),
        "bom-ref": purl(p.name, p.version),
      })),
    };

    const out = join(SBOM_DIR, `${stackName}.${logicalId}.cdx.json`);
    writeFileSync(out, JSON.stringify(sbom, null, 2));
    console.log(`✔ ${cdkPath}: ${packages.length} packages → ${out}`);
    count++;
  }
}

if (count === 0) {
  console.error("No Lambda functions with a metafile found. Run `npx cdk synth` first.");
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Synthesize and create the SBOMs

npx cdk synth --quiet
npx tsx scripts/sbom-from-cdk-out.ts
Enter fullscreen mode Exit fullscreen mode

Expected result:

✔ BikeRentalStack/RentBikeFn/Resource: 2 packages → sbom/BikeRentalStack.RentBikeFn43E744CA.cdx.json
✔ BikeRentalStack/ListStationsFn/Resource: 1 packages → sbom/BikeRentalStack.ListStationsFn80EEF5EA.cdx.json
Enter fullscreen mode Exit fullscreen mode

An excerpt from the generated file shows the structure: metadata about the function, followed by the components with their purl.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "metadata": {
    "component": {
      "type": "application",
      "name": "BikeRentalStack/RentBikeFn",
      "properties": [{ "name": "cdk:logicalId", "value": "RentBikeFn43E744CA" }]
    }
  },
  "components": [
    { "type": "library", "name": "ulid", "version": "3.0.2", "purl": "pkg:npm/ulid@3.0.2" },
    { "type": "library", "name": "zod", "version": "4.6.7", "purl": "pkg:npm/zod@4.6.7" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Search when it matters

The second script answers the core question. It searches all SBOM files for a package, optionally with an exact version. On a hit it exits with code 1, so you can use it directly as a gate.

// scripts/find-package.ts
// Searches all SBOM files for a package, optionally with a version.
// Usage: npx tsx scripts/find-package.ts zod           (all versions)
//        npx tsx scripts/find-package.ts zod@4.6.7     (exactly this version)
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const query = process.argv[2];
const SBOM_DIR = process.argv[3] ?? "sbom";
if (!query) {
  console.error("Please pass a package, e.g. zod or zod@4.6.7");
  process.exit(2);
}

// Split "@scope/name@1.2.3" correctly: the last @ after position 0
const at = query.lastIndexOf("@");
const [name, version] = at > 0 ? [query.slice(0, at), query.slice(at + 1)] : [query, undefined];

type Sbom = {
  metadata: { component: { name: string } };
  components: { name: string; version: string }[];
};

let hits = 0;
for (const file of readdirSync(SBOM_DIR).filter((f) => f.endsWith(".cdx.json"))) {
  const sbom = JSON.parse(readFileSync(join(SBOM_DIR, file), "utf8")) as Sbom;
  for (const c of sbom.components) {
    if (c.name === name && (!version || c.version === version)) {
      console.log(`${sbom.metadata.component.name}\t${c.name}@${c.version}`);
      hits++;
    }
  }
}

console.log(hits === 0 ? `Not found: ${query}` : `${hits} hit(s)`);
// Exit code 1 on hits – handy as a gate in the pipeline
process.exit(hits > 0 ? 1 : 0);
Enter fullscreen mode Exit fullscreen mode
npx tsx scripts/find-package.ts zod
Enter fullscreen mode Exit fullscreen mode
BikeRentalStack/ListStationsFn  zod@4.6.7
BikeRentalStack/RentBikeFn  zod@4.6.7
2 hit(s)
Enter fullscreen mode Exit fullscreen mode

Step 6: In the pipeline as artifact and gate

In the pipeline, you create the SBOMs on every build, store them as an artifact for the commit and check them against a blocklist. The blocklist is a plain text file with one package@version per line. When an incident like the flatmap-stream case from 2018 happens, you add the version, and no build containing it gets through anymore.

# sbom-blocklist.txt
# one package@version per line
flatmap-stream@0.1.1
Enter fullscreen mode Exit fullscreen mode
# .github/workflows/sbom.yml
name: sbom
on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  sbom:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npx cdk synth --quiet
      - run: npx tsx scripts/sbom-from-cdk-out.ts
      # Gate: fail the build if a known compromised version is bundled
      - name: Check blocklist
        run: |
          while read -r pkg; do
            case "$pkg" in ""|\#*) continue;; esac
            npx tsx scripts/find-package.ts "$pkg" || { echo "::error::$pkg is bundled"; exit 1; }
          done < sbom-blocklist.txt
      - uses: actions/upload-artifact@v7
        with:
          name: sbom-${{ github.sha }}
          path: sbom/
          retention-days: 90
Enter fullscreen mode Exit fullscreen mode

Keep the artifacts at least as long as a deployment typically stays live. For functions that go months without a redeploy, 90 days is not enough. In that case it pays off to also write the SBOMs to an S3 bucket with versioning.

Querying the Security Hub SBOM inventory with a script

The managed route complements your build SBOM well, especially for functions not built with esbuild, such as Python or Java. First, enable Lambda scanning in Inspector, in this example for the current account and Region:

aws inspector2 enable --resource-types LAMBDA
Enter fullscreen mode Exit fullscreen mode

After that, the inventory fills up gradually. For queries, the Security Hub API offers GetSoftwareV2 (search packages) and GetSoftwareResourcesV2 (resources for a package). The second one requires the package type, so the script calls the first one beforehand. For a test, the managed policy AWSSecurityHubReadOnlyAccess is sufficient.

// scripts/where-is-package.ts
// Queries the AWS Security Hub SBOM inventory:
// which resources (e.g. Lambda functions) contain a package?
// Usage: npx tsx scripts/where-is-package.ts zod [4.6.7]
import {
  GetSoftwareResourcesV2Command,
  GetSoftwareV2Command,
  SecurityHubClient,
} from "@aws-sdk/client-securityhub";

const [name, version] = process.argv.slice(2);
if (!name) {
  console.error("Usage: npx tsx scripts/where-is-package.ts <package> [version]");
  process.exit(2);
}

const client = new SecurityHubClient({});

// 1. Look up the package in the inventory – this returns the PackageType,
//    which the second query requires.
const stringFilters = [
  { FieldName: "PackageName" as const, Filter: { Value: name, Comparison: "EQUALS" as const } },
  ...(version
    ? [{ FieldName: "PackageVersion" as const, Filter: { Value: version, Comparison: "EQUALS" as const } }]
    : []),
];

const software = await client.send(
  new GetSoftwareV2Command({
    Filters: { CompositeFilters: [{ StringFilters: stringFilters, Operator: "AND" }] },
    MaxResults: 50,
  }),
);

const packages = software.Software ?? [];
if (packages.length === 0) {
  console.log(`No match for ${name}${version ? "@" + version : ""} in the SBOM inventory.`);
  process.exit(0);
}

// 2. For each version found, fetch the affected resources (with pagination)
for (const pkg of packages) {
  console.log(`\n${pkg.PackageName}@${pkg.PackageVersion} (${pkg.PackageType}) – ${pkg.ResourceCount} resources`);
  let nextToken: string | undefined;
  do {
    const page = await client.send(
      new GetSoftwareResourcesV2Command({
        PackageName: pkg.PackageName,
        PackageType: pkg.PackageType,
        PackageVersion: pkg.PackageVersion,
        NextToken: nextToken,
        MaxResults: 100,
      }),
    );
    for (const r of page.SoftwareResources ?? []) {
      console.log(`  ${r.ResourceType}\t${r.AccountId}\t${r.Region}\t${r.ResourceId}`);
    }
    nextToken = page.NextToken;
  } while (nextToken);
}
Enter fullscreen mode Exit fullscreen mode
npx tsx scripts/where-is-package.ts zod 4.6.7
Enter fullscreen mode Exit fullscreen mode

If you need the SBOMs as files, for an audit or your own tooling, Inspector exports them to an S3 bucket. Supported formats are CYCLONEDX_1_4 and SPDX_2_3. Inspector requires a customer managed, symmetric KMS key in the same Region as the bucket, and both need a policy that grants Inspector access.

aws inspector2 create-sbom-export \
  --report-format CYCLONEDX_1_4 \
  --resource-filter-criteria '{"resourceType":[{"comparison":"EQUALS","value":"AWS_LAMBDA_FUNCTION"}]}' \
  --s3-destination bucketName=bike-rental-sbom,keyPrefix=inspector,kmsKeyArn=arn:aws:kms:eu-central-1:111122223333:key/EXAMPLE

aws inspector2 get-sbom-export --report-id <reportId from the response>
Enter fullscreen mode Exit fullscreen mode

Which route fits when?

You do not have to pick one. They answer slightly different questions, and together they close each other’s gaps.

SBOM from the build (metafile) Inspector + Security Hub npm sbom
Accuracy exactly what is in the bundle depends on what the scanner finds in the artifact everything in the lockfile, not per function
Timing before deploy, usable as a gate after deploy, a few hours of delay before deploy
Scope per repository whole organization per project
Vulnerability matching needs a separate tool (e.g. Grype, OSV) included as Inspector findings needs a separate tool
Effort maintain the script, keep artifacts enable it, ongoing cost per function one command

My suggestion for most teams: build SBOM as gate and archive, Inspector for ongoing vulnerability matching and the big picture. If the two sources disagree, that is a valuable signal, by the way. Usually there is an old function behind it that nobody has redeployed for months.

Common Lambda SBOM mistakes

  • Only looking at the lockfile. It describes the state of the repository, not the state in production.
  • esbuild missing locally. CDK then bundles inside a Docker container. That works, but the metafile paths refer to the container. Check the output before you trust the result.
  • Forgetting layers. Packages from Lambda layers are not in the function’s metafile. Layers need their own SBOM.
  • Not keeping SBOMs. An SBOM that only lived in a CI log will not help you in the next incident.
  • Overlooking external packages. Anything you exclude via externalModules (by default the AWS SDK for Node.js 18 and later) is not in the bundle. That is correct, but you should know that these packages come from the runtime.

Cost and security

The self-generated SBOM costs practically nothing beyond a few seconds of build time. With Inspector, you pay per scanned function per month. With hundreds of functions, keep an eye on the bill, and the InspectorExclusion tag lets you deliberately exclude individual functions, such as pure test functions. Also keep in mind: an SBOM is a map of your dependencies. It is just as useful to attackers as it is to you. Store the files in a private bucket or as a private CI artifact, not publicly in the repository.

My take

I see the SBOM inventory in Security Hub as real progress, because it finally makes “where do we use this?” answerable in one central place. For container landscapes, that is almost the whole solution. For serverless applications in TypeScript, I do not think it is enough on its own, as long as it is unclear how much a scanner detects in a bundled artifact. If you plan to rely on an inventory during an incident, test beforehand that it is complete.

So this is how I would build it today: generate the SBOM in the build, because the build knows most precisely what is inside. Use the blocklist as a gate, because it takes effect within minutes during an incident. And run Inspector in parallel, because it finds new vulnerabilities even in functions nobody is touching right now. If you have already worked on supply chain protection, for example with a waiting period for new npm versions, the SBOM is the logical next step. How the cooldown works is covered in my post on npm min-release-age.

FAQ

What is an SBOM?

An SBOM (software bill of materials) is a machine-readable list of all packages and versions in a piece of software. Common formats are CycloneDX and SPDX.

Does the SBOM inventory in Security Hub cost extra?

According to AWS, the inventory is included in Security Hub Essentials at no additional cost. You do need Amazon Inspector, though, and Lambda scanning is billed per function per month.

Which formats does Amazon Inspector use to export SBOMs?

Exports via CreateSbomExport support CycloneDX 1.4 and SPDX 2.3. The target is an S3 bucket encrypted with a customer managed KMS key.

Do I need an SBOM if I already use Dependabot?

Dependabot looks at your repository and suggests updates. An SBOM of the deployed artifact shows what is actually running. The two complement each other, especially for functions that are rarely redeployed.

Your next step

Pick a CDK project, enable metafile: true and run the script once. You will immediately see which packages live in which function, and maybe also that one dependency nobody remembered. I am curious: how do you answer “where do we use package X?” today, and how long does it take?

Sources

This post was created with AI assistance.


👉 Next step: Curious what your own Lambda functions cost? Try my Lambda cost calculator – it runs right in your browser.

📬 Stay updated: New articles on serverless, AI and leading tech teams via RSS, browser push or the weekly newsletter (in German).

Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de/en

Top comments (0)