Generate SBOM from OCI container made with Nix

Hello,

I would like to create a couple of OCI images embedding their own SBOM.

The idea is to show that when creating an OCI image with Nix, it is easy to extract its SBOM (in any format, text or SPDX).

I tried to use bombon (GitHub - nikstur/bombon: Nix CycloneDX Software Bills of Materials (SBOMs) thanks @nikstur !) but the resulting file is:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.3",
  "version": 1,
  "serialNumber": "urn:uuid:23ca648e-7e18-47ae-a80c-0f1098d1dbfa",
  "metadata": {
    "tools": [
      {
        "vendor": "nikstur",
        "name": "bombon",
        "version": "c42b8aa9a666db7d61c9e6d4b5a5ed41b6215054"
      }
    ],
    "component": {
      "type": "application",
      "bom-ref": "urn:uuid:734d35e1-cd4f-43f3-bf09-589d456b2c7c",
      "name": "php-web.tar.gz",
      "version": "",
      "purl": "pkg:nix/php-web.tar.gz@"
    }
  },
  "components": []
}

We can clearly see that the OCI image is opaque, it’s just a .tar.gz file, and not information is extracted out of it. I wish I could have information about caddy, php and all the transitive dependencies embedded in the image.

Find the flake here:

{
  description = "Nix, OCI and SBOM";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    bombon.url = "github:nikstur/bombon";
    bombon.inputs.nixpkgs.follows = "nixpkgs";
    systems.url = "github:nix-systems/default";
  };

  outputs = inputs@{ self, flake-parts, systems, bombon, ... }: flake-parts.lib.mkFlake { inherit inputs; } {
    systems = import systems;

    perSystem = { config, self', inputs', pkgs, system, lib, ... }: {
      packages = {
        oci-image-sbom = bombon.lib.${system}.buildBom config.packages.oci-image { };

        oci-image = pkgs.dockerTools.buildLayeredImage {
          name = "php-web";
          tag = "latest";

          contents = [
            pkgs.php81
            pkgs.caddy
            pkgs.dockerTools.caCertificates
            pkgs.fakeNss
            (pkgs.writeScriptBin "start-server" ''
              #!${pkgs.runtimeShell}
              php-fpm -D -y /etc/php-fpm.d/www.conf.default
              caddy run --adapter caddyfile --config ${./Caddyfile}
            '')
          ];

          extraCommands = ''
            mkdir -p tmp
            chmod 1777 tmp
          '';

          config = {
            Cmd = [ "start-server" ];
            ExposedPorts = {
              "80/tcp" = {};
              "443/tcp" = {};
            };
          };
        };
      };
    };
  };
}

Thanks!

1 Like

More info, running:

❯ nix build .#oci-image
❯ nix-store --query --tree /nix/store/3lzkiv155gx88aasw673y1lvrrh9k6f2-php-web.tar.gz
/nix/store/3lzkiv155gx88aasw673y1lvrrh9k6f2-php-web.tar.gz
~/C/ecphp/dsocc/poc-php-docker-caas > php ❯

Doesn’t print anything relevant.

@K900 on Matrix suggested to use the .drv file, by doing:

❯ nix-store --query --deriver /nix/store/3lzkiv155gx88aasw673y1lvrrh9k6f2-php-web.tar.gz
/nix/store/sqckjswmcnmkl93715cj1wkmhag20p3z-php-web.tar.gz.drv
❯ nix-store --query --tree /nix/store/sqckjswmcnmkl93715cj1wkmhag20p3z-php-web.tar.gz.drv

That’s printing all the buildtime dependencies, dependencies required to build the OCI container.

In this case, I just want the list of included dependencies(runtime?) in the container. Is there a way to do that?

How to print the SBOM of an OCI container made with Nix?

Have you tried GitHub - tiiuae/sbomnix: A suite of utilities to help with software supply chain challenges on nix targets ?

Edit: I guess nix run github:tiiuae/sbomnix#sbomnix -- .#oci-image --buildtime is not what you want. The issue is with buildLayeredImage returning a simple tarball. As a workaround, add a second package that is just a symlinkJoin of the contents you pass to buildLayeredImage, then do nix run github:tiiuae/sbomnix#sbomnix -- .#contents

Thanks ! That works !!!

Here’s the final flake.nix file:

{
  description = "Flake for building docker image";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    bombon.url = "github:nikstur/bombon";
    bombon.inputs.nixpkgs.follows = "nixpkgs";
    systems.url = "github:nix-systems/default";
  };

  outputs = inputs@{ self, flake-parts, systems, bombon, ... }: flake-parts.lib.mkFlake { inherit inputs; } {
    systems = import systems;

    perSystem = { config, self', inputs', pkgs, system, lib, ... }: {
      packages = {
        oci-image-sbom = bombon.lib.${system}.buildBom config.packages.oci-image-content { };

        oci-image-content = pkgs.symlinkJoin {
          name = "oci-image-content";
          paths = [
            pkgs.php81
            pkgs.caddy
            pkgs.dockerTools.caCertificates
            pkgs.fakeNss
            (pkgs.writeScriptBin "start-server" ''
              #!${pkgs.runtimeShell}
              php-fpm -D -y /etc/php-fpm.d/www.conf.default
              caddy run --adapter caddyfile --config ${./Caddyfile}
            '')
            ./.
          ];
        };

        oci-image = pkgs.dockerTools.buildLayeredImage {
          name = "php-web";
          tag = "latest";

          contents = config.packages.oci-image-content;

          extraCommands = ''
            mkdir -p tmp
            chmod 1777 tmp
          '';

          config = {
            Cmd = [ "start-server" ];
            ExposedPorts = {
              "80/tcp" = {};
              "443/tcp" = {};
            };
            Labels = {
              SBOM = (builtins.readFile "${config.packages.oci-image-sbom}");
            };
          };
        };
      };
    };
  };
}

To test it:

nix build .#oci-image
docker load < result
docker inspect php-web:latest | jq -r '.[0].Config.Labels["SBOM"]' | jq

@wamserma I wish I could also test this project GitHub - tiiuae/sbomnix: A suite of utilities to help with software supply chain challenges on nix targets but I can’t find a way to generate the SBOM through an API.
Do you have a clue on how I could achieve this?

AFAIK sbomnix doesn’t expose an API, but you can probably consume the sbomnix derivation from https://github.com/tiiuae/sbomnix/blob/bbfe0949ab8875f5c98dcdd1d81d2721676b4b26/nix/packages.nix with a runCommand …

Yes, I’ve been experimenting with:

  oci-image-sbom = pkgs.runCommand "oci-image-sbom" {
    nativeBuildInputs = [ pkgs.sbomnix ];
  } ''
    echo "Running sbomnix --type runtime ${config.packages.oci-image-content}"
    sbomnix --type runtime ${config.packages.oci-image-content}
    cp sbom.spdx.json $out
  '';

But got an issue:

❯ nix build .#oci-image
error:
       … while calling the 'derivationStrict' builtin

         at /builtin/derivation.nix:9:12: (source not available)

       … while evaluating derivation 'php-web.tar.gz'
         whose name attribute is located at /nix/store/3qsyy1d9wlxy0zaa5l27nwis7cjf93qs-source/pkgs/stdenv/generic/make-derivation.nix:352:7

       … while evaluating attribute 'buildCommand' of derivation 'php-web.tar.gz'

         at /nix/store/3qsyy1d9wlxy0zaa5l27nwis7cjf93qs-source/pkgs/build-support/trivial-builders/default.nix:98:16:

           97|         enableParallelBuilding = true;
           98|         inherit buildCommand name;
             |                ^
           99|         passAsFile = [ "buildCommand" ]

       (stack trace truncated; use '--show-trace' to show the full trace)

       error: builder for '/nix/store/k0qydyjdh8xhhxm7npv164z49zj196dv-oci-image-sbom.drv' failed with exit code 1;
       last 23 log lines:
       > Running sbomnix --type runtime /nix/store/rvksazg0kfl5sn8s77473xnzy12ib2zl-oci-image-content
       > WARNING  Command line argument '--meta' missing: SBOM will not include license information (see '--help' for more details)
       > Traceback (most recent call last):
       >   File "/nix/store/xs8sgwcvdj8xp32k1lmf701vz7ysmky1-sbomnix-1.4.5/bin/.sbomnix-wrapped", line 9, in <module>
       >     sys.exit(main())
       >              ^^^^^^
       >   File "/nix/store/xs8sgwcvdj8xp32k1lmf701vz7ysmky1-sbomnix-1.4.5/lib/python3.11/site-packages/sbomnix/main.py", line 87, in main
       >     sbomdb = SbomDb(target_path, runtime, buildtime, args.meta)
       >              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       >   File "/nix/store/xs8sgwcvdj8xp32k1lmf701vz7ysmky1-sbomnix-1.4.5/lib/python3.11/site-packages/sbomnix/sbomdb.py", line 45, in __init__
       >     self.target_deriver = find_deriver(nix_path)
       >                           ^^^^^^^^^^^^^^^^^^^^^^
       >   File "/nix/store/xs8sgwcvdj8xp32k1lmf701vz7ysmky1-sbomnix-1.4.5/lib/python3.11/site-packages/sbomnix/nix.py", line 119, in find_deriver
       >     qpi_deriver = exec_cmd(["nix-store", "-qd", path]).strip()
       >                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       >   File "/nix/store/xs8sgwcvdj8xp32k1lmf701vz7ysmky1-sbomnix-1.4.5/lib/python3.11/site-packages/sbomnix/utils.py", line 113, in exec_cmd
       >     raise error
       >   File "/nix/store/xs8sgwcvdj8xp32k1lmf701vz7ysmky1-sbomnix-1.4.5/lib/python3.11/site-packages/sbomnix/utils.py", line 103, in exec_cmd
       >     ret = subprocess.run(cmd, capture_output=True, encoding="utf-8", check=True)
       >           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       >   File "/nix/store/w4fvvhkzb0ssv0fw5j34pw09f0qw84w8-python3-3.11.7/lib/python3.11/subprocess.py", line 571, in run
       >     raise CalledProcessError(retcode, process.args,
       > subprocess.CalledProcessError: Command '['nix-store', '-qd', '/nix/store/rvksazg0kfl5sn8s77473xnzy12ib2zl-oci-image-content']' returned non-zero exit status 1.
       For full logs, run 'nix log /nix/store/k0qydyjdh8xhhxm7npv164z49zj196dv-oci-image-sbom.drv'.
✘

Maybe don’t evaluate at buildtime, that is, use writeSciptBin instead of runCommand and the nix run .#oci-image-sbom instead of nix build .#oci-image-sbom. You lose the caching of the SBOM in the Nix store but also avoid calling nix-store from a build (which is impure).

But I need to evaluate it at buildtime to inject the SBOM in the OCI image labels…

Let’s ask the author: @henrirosten any ideas?

pkgs.sbomnix in nixpkgs is pretty badly outdated, can you try with the latest version from the sbomnix repo github:tiiuae/sbomnix#sbomnix?

Also, --verbose=2 might give more info as to why it fails.

I’m trying to do that in pkgs.runCommand as such:

        oci-image-sbom = pkgs.runCommand "oci-image-sbom" {
          nativeBuildInputs = [ pkgs.nix pkgs.sbomnix ];
        } ''
          echo "Running nix run github:tiiuae/sbomnix#sbomnix ${config.packages.oci-image-content.drvPath}"
          nix --extra-experimental-features 'nix-command flakes' run github:tiiuae/sbomnix#sbomnix ${config.packages.oci-image-content.drvPath}
          cp sbom.spdx.json $out
        '';

But then I got:

❯ nix build .#oci-image
error:
       … while calling the 'derivationStrict' builtin

         at /builtin/derivation.nix:9:12: (source not available)

       … while evaluating derivation 'php-web.tar.gz'
         whose name attribute is located at /nix/store/3qsyy1d9wlxy0zaa5l27nwis7cjf93qs-source/pkgs/stdenv/generic/make-derivation.nix:352:7

       … while evaluating attribute 'buildCommand' of derivation 'php-web.tar.gz'

         at /nix/store/3qsyy1d9wlxy0zaa5l27nwis7cjf93qs-source/pkgs/build-support/trivial-builders/default.nix:98:16:

           97|         enableParallelBuilding = true;
           98|         inherit buildCommand name;
             |                ^
           99|         passAsFile = [ "buildCommand" ]

       (stack trace truncated; use '--show-trace' to show the full trace)

       error: builder for '/nix/store/b940fhdp99hgzsaysaddp8kbc7fy0w2g-oci-image-sbom.drv' failed with exit code 1;
       last 3 log lines:
       > Running nix run github:tiiua/sbomnix#sbomnix /nix/store/yaxc23va5ya92scj39xwkrl65wpdb84b-oci-image-content.drv
       > warning: you don't have Internet access; disabling some network-dependent features
       > error: creating directory '/nix/var': Permission denied
       For full logs, run 'nix log /nix/store/b940fhdp99hgzsaysaddp8kbc7fy0w2g-oci-image-sbom.drv'.
✘

Can you try running sbomnix with adding the option --verbose=2 to the command to get some more info as to why it fails?

Another test you could try: is it possible for you to run the failing sbomnix command after the failure from command-line using the same target derivation (/nix/store/yaxc23va5ya92scj39xwkrl65wpdb84b-oci-image-content.drv)?

Running your flake from my regular command line works well, it’s running it within Nix, in isolation that doesn’t.

❯ nix run github:tiiuae/sbomnix#sbomnix /nix/store/8wszsvrbcby2hnsg22073cj4pg8ya6y4-oci-image-content.drv

INFO     Evaluating '/nix/store/8wszsvrbcby2hnsg22073cj4pg8ya6y4-oci-image-content.drv'
INFO     Try force-realising store-path '/nix/store/8wszsvrbcby2hnsg22073cj4pg8ya6y4-oci-image-content.drv'
INFO     Loading runtime dependencies referenced by '/nix/store/8wszsvrbcby2hnsg22073cj4pg8ya6y4-oci-image-content.drv'
INFO     Wrote: sbom.cdx.json
INFO     Wrote: sbom.spdx.json
INFO     Wrote: sbom.csv

The issue I see in the log previously posted here are:

> Running nix run github:tiiua/sbomnix#sbomnix /nix/store/yaxc23va5ya92scj39xwkrl65wpdb84b-oci-image-content.drv
> warning: you don't have Internet access; disabling some network-dependent features
> error: creating directory '/nix/var': Permission denied

I posted the small project I plan to use at work here: https://code.europa.eu/pol/ec-lib/

Maybe this might help some people here.