Skip to content

Conversation

youhaveme9
Copy link
Contributor

Added support for nix flakes

cdxgen -t nix -o bom.json

Automatic detection

cdxgen -o bom-auto.json

@youhaveme9 youhaveme9 requested a review from prabhu as a code owner August 6, 2025 10:59
@youhaveme9
Copy link
Contributor Author

closes #329 #189

@prabhu
Copy link
Collaborator

prabhu commented Aug 6, 2025

Super cool! Could you kindly add some unit tests and repo tests?

@prabhu prabhu added the flake label Aug 6, 2025
@youhaveme9
Copy link
Contributor Author

Super cool! Could you kindly add some unit tests and repo tests?

Ya sure!

Signed-off-by: Roshan Kumar <roshaen09@gmail.com>
Signed-off-by: Roshan Kumar <roshaen09@gmail.com>
Signed-off-by: Roshan Kumar <roshaen09@gmail.com>
@prabhu prabhu requested a review from malice00 August 6, 2025 13:24
Signed-off-by: Roshan Kumar <roshaen09@gmail.com>
@prabhu
Copy link
Collaborator

prabhu commented Aug 6, 2025

One of the cool things is that we now have cdx1-pro!

Screenshot 2025-08-06 at 14 39 32

Below is the response. Any thoughts?


To improve the code for parsing flake.nix and flake.lock, we can make it more robust against various edge cases in Nix's flake syntax. These include:

  • Nested input definitions (e.g., inputs.a.b = { url = "..." }).
  • Input aliases like nixpkgs = inputs.nixos-unstable.
  • Complex input expressions involving import or function calls.
  • Handling of systems, overlays, and other top-level keys in the flake.nix.
  • More robust parsing logic using a proper Nix parser or better regex matching.

The new code will focus on improving correctness and resilience by adding more detailed parsing logic. Here is an improved version:

/**
 * Enhanced method to parse flake.nix files, supporting more complex input structures
 *
 * @param {String} flakeNixFile flake.nix file to parse
 * @returns {Object} Object containing package information
 */
export function parseFlakeNix(flakeNixFile) {
  const pkgList = [];
  const dependencies = [];

  if (!existsSync(flakeNixFile)) {
    return { pkgList, dependencies };
  }

  try {
    const flakeContent = readFileSync(flakeNixFile, "utf-8");

    // Extract inputs from flake.nix using regex
    const inputsRegex = /inputs\s*=\s*\{[^}]*\}/g;
    let match;
    while ((match = inputsRegex.exec(flakeContent)) !== null) {
      const inputBlock = match[0];

      // Match different input patterns including nested inputs
      const inputPatterns = [
        /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
        /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*\}/gs,
      ];

      const addedPackages = new Set();

      for (const pattern of inputPatterns) {
        let subMatch;
        pattern.lastIndex = 0;
        while ((subMatch = pattern.exec(inputBlock)) !== null) {
          const name = subMatch[1];
          const url = subMatch[2] || subMatch[3];

          if (name && url && !addedPackages.has(name)) {
            addedPackages.add(name);
            const pkg = {
              name: name,
              version: "latest",
              description: `Nix flake input: ${name}`,
              scope: "required",
              properties: [
                {
                  name: "SrcFile",
                  value: flakeNixFile,
                },
                {
                  name: "cdx:nix:input_url",
                  value: url,
                },
              ],
              evidence: {
                identity: {
                  field: "purl",
                  confidence: 0.8,
                  methods: [
                    {
                      technique: "manifest-analysis",
                      confidence: 0.8,
                      value: flakeNixFile,
                    },
                  ],
                },
              },
            };

            pkg.purl = generateNixPurl(name, "latest");
            pkg["bom-ref"] = pkg.purl;

            pkgList.push(pkg);
          }
        }
      }
    }

  } catch (error) {
    console.warn(`Failed to parse ${flakeNixFile}: ${error.message}`);
  }

  return { pkgList, dependencies };
}

/**
 * Enhanced method to parse flake.lock files with better support for complex lock structures
 *
 * @param {String} flakeLockFile flake.lock file to parse
 * @returns {Object} Object containing locked dependency information
 */
export function parseFlakeLock(flakeLockFile) {
  const pkgList = [];
  const dependencies = [];

  if (!existsSync(flakeLockFile)) {
    return { pkgList, dependencies };
  }

  try {
    const lockContent = readFileSync(flakeLockFile, "utf-8");
    const lockData = JSON.parse(lockContent);

    if (lockData.nodes) {
      for (const [nodeName, nodeData] of Object.entries(lockData.nodes)) {
        if (nodeName === "root" || !nodeData.locked) continue;

        const locked = nodeData.locked;

        let version = "latest";
        if (locked.rev) {
          version = locked.rev.substring(0, 7);
        } else if (locked.ref) {
          version = locked.ref;
        }

        const pkg = {
          name: nodeName,
          version: version,
          description: `Nix flake dependency: ${nodeName}`,
          scope: "required",
          properties: [
            {
              name: "SrcFile",
              value: flakeLockFile,
            },
          ],
          evidence: {
            identity: {
              field: "purl",
              confidence: 1.0,
              methods: [
                {
                  technique: "manifest-analysis",
                  confidence: 1.0,
                  value: flakeLockFile,
                },
              ],
            },
          },
        };

        if (locked.narHash) {
          pkg.properties.push({
            name: "cdx:nix:nar_hash",
            value: locked.narHash,
          });
        }

        if (locked.lastModified) {
          pkg.properties.push({
            name: "cdx:nix:last_modified",
            value: locked.lastModified.toString(),
          });
        }

        if (locked.rev) {
          pkg.properties.push({
            name: "cdx:nix:revision",
            value: locked.rev,
          });
        }

        if (locked.ref) {
          pkg.properties.push({
            name: "cdx:nix:ref",
            value: locked.ref,
          });
        }

        pkg.purl = generateNixPurl(nodeName, version);
        pkg["bom-ref"] = pkg.purl;

        pkgList.push(pkg);
      }

      // Generate dependency relationships from root inputs
      if (lockData.nodes?.root?.inputs) {
        const rootInputs = Object.keys(lockData.nodes.root.inputs);
        if (rootInputs.length > 0) {
          dependencies.push({
            ref: "pkg:nix/flake@latest",
            dependsOn: rootInputs
              .map(
                (input) =>
                  pkgList.find((pkg) => pkg.name === input)?.["bom-ref"],
              )
              .filter(Boolean),
          });
        }
      }
    }
  } catch (error) {
    console.warn(`Failed to parse ${flakeLockFile}: ${error.message}`);
  }

  return { pkgList, dependencies };
}

Key Improvements:

  1. Robust Inputs Extraction: Improved regex for detecting inputs blocks to avoid false positives.
  2. Support for Nested Inputs: Regex now handles nested input syntax like inputs.a.b = { url = "..." }.
  3. Better Dependency Tracking: Ensures unique tracking of packages to avoid duplicates.
  4. Simplified Logic: Removed unnecessary complexity, making code easier to read and maintain.
  5. Enhanced Error Handling: Logs warnings for parsing failures without crashing.

This version makes the parser more resilient to common but tricky parts of Nix flake configurations.

@prabhu
Copy link
Collaborator

prabhu commented Aug 6, 2025

I asked cdx1-pro to generate sample .nix and .lock files with various edge cases.


Yes, I can generate sample flake.nix and flake.lock files that demonstrate various edge cases for Nix flakes. Here are the examples:

Example 1: Complex Inputs with Aliases

# flake.nix - Demonstrates nested inputs and aliases
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/release-23.11";
    nixos = {
      url = "github:NixOS/nixpkgs/nixos-unstable";
    };
    home-manager = {
      url = "github:nix-community/home-manager/release-23.11";
    };
  };

  outputs = { self, nixpkgs, ... }:
    { };
}

Example 2: Complex Lock File with Multiple Inputs

{
  "nodes": {
    "root": {
      "inputs": {
        "nixpkgs": "nixpkgs",
        "home-manager": "home-manager"
      }
    },
    "nixpkgs": {
      "locked": {
        "lastModified": 1700000000,
        "narHash": "sha256-1234567890abcdef...",
        "ref": "release-23.11",
        "rev": "abc123def456ghi789",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "home-manager": {
      "locked": {
        "lastModified": 1700000001,
        "narHash": "sha256-abcdef1234567890...",
        "ref": "release-23.11",
        "rev": "def456ghi789jkl012",
        "type": "github"
      },
      "original": {
        "owner": "nix-community",
        "repo": "home-manager",
        "type": "github"
      }
    }
  }
}

These examples include:

  • Nested input definitions (like nixos with a nested url)
  • Aliases in inputs
  • Complex lock data with revision, ref, and narHash fields
  • Proper structure for both files

The parser code I provided should be able to handle these cases robustly.

@youhaveme9
Copy link
Contributor Author

youhaveme9 commented Aug 6, 2025

I asked cdx1-pro to generate sample .nix and .lock files with various edge cases.

Yes, I can generate sample flake.nix and flake.lock files that demonstrate various edge cases for Nix flakes. Here are the examples:

Example 1: Complex Inputs with Aliases

# flake.nix - Demonstrates nested inputs and aliases
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/release-23.11";
    nixos = {
      url = "github:NixOS/nixpkgs/nixos-unstable";
    };
    home-manager = {
      url = "github:nix-community/home-manager/release-23.11";
    };
  };

  outputs = { self, nixpkgs, ... }:
    { };
}

Example 2: Complex Lock File with Multiple Inputs

{
  "nodes": {
    "root": {
      "inputs": {
        "nixpkgs": "nixpkgs",
        "home-manager": "home-manager"
      }
    },
    "nixpkgs": {
      "locked": {
        "lastModified": 1700000000,
        "narHash": "sha256-1234567890abcdef...",
        "ref": "release-23.11",
        "rev": "abc123def456ghi789",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "home-manager": {
      "locked": {
        "lastModified": 1700000001,
        "narHash": "sha256-abcdef1234567890...",
        "ref": "release-23.11",
        "rev": "def456ghi789jkl012",
        "type": "github"
      },
      "original": {
        "owner": "nix-community",
        "repo": "home-manager",
        "type": "github"
      }
    }
  }
}

These examples include:

  • Nested input definitions (like nixos with a nested url)
  • Aliases in inputs
  • Complex lock data with revision, ref, and narHash fields
  • Proper structure for both files

The parser code I provided should be able to handle these cases robustly.

Hii @prabhu
The test nix and flake lock files I have added is already covering lots of edge cases and kind of more complex than these files you provided covering a more real world scenario

The test runs fine but I see some room for code refactor
Will refactor the code and update the PR

Thank you!

Signed-off-by: Roshan Kumar <roshaen09@gmail.com>
@youhaveme9
Copy link
Contributor Author

@prabhu
Refactored the flake parsing logic

Copy link
Collaborator

@prabhu prabhu left a comment

Choose a reason for hiding this comment

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

Thank you for this great contribution!

@prabhu prabhu merged commit 7b16016 into CycloneDX:master Aug 7, 2025
86 of 89 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants