Cybersecurity-Projects/PROJECTS/intermediate/sbom-generator-vulnerabilit.../internal/parser/node.go

210 lines
4.7 KiB
Go

// ©AngelaMos | 2026
// node.go
package parser
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/CarterPerez-dev/bomber/pkg/types"
"gopkg.in/yaml.v3"
)
var nodeSemverRe = regexp.MustCompile(`\d+\.\d+\.\d+`)
type NodeParser struct{}
func NewNodeParser() *NodeParser {
return &NodeParser{}
}
func (p *NodeParser) Ecosystem() types.Ecosystem {
return types.EcosystemNode
}
func (p *NodeParser) Detect(dir string) bool {
_, err := os.Stat(filepath.Join(dir, "package.json"))
return err == nil
}
func (p *NodeParser) Parse(dir string) (*types.DependencyGraph, error) {
pkgPath := filepath.Join(dir, "package.json")
data, err := os.ReadFile(pkgPath)
if err != nil {
return nil, fmt.Errorf("read package.json: %w", err)
}
var pkg packageJSON
if err := json.Unmarshal(data, &pkg); err != nil {
return nil, fmt.Errorf("parse package.json: %w", err)
}
root := types.Package{
Name: pkg.Name,
Version: pkg.Version,
Ecosystem: types.EcosystemNode,
PURL: fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(pkg.Name), pkg.Version),
Direct: true,
}
graph := types.NewDependencyGraph(root)
directNames := make(map[string]bool)
for name := range pkg.Dependencies {
directNames[name] = true
}
for name := range pkg.DevDependencies {
directNames[name] = true
}
lockPath := filepath.Join(dir, "pnpm-lock.yaml")
lockData, err := os.ReadFile(lockPath)
if err == nil {
parsePnpmLock(lockData, graph, root.PURL, directNames)
} else {
parseFromPackageJSON(pkg, graph, root.PURL)
}
return graph, nil
}
type packageJSON struct {
Name string `json:"name"`
Version string `json:"version"`
Dependencies map[string]string `json:"dependencies"`
DevDependencies map[string]string `json:"devDependencies"`
}
type pnpmLockfile struct {
Packages map[string]pnpmPackage `yaml:"packages"`
Snapshots map[string]pnpmSnapshot `yaml:"snapshots"`
}
type pnpmPackage struct {
Resolution struct {
Integrity string `yaml:"integrity"`
} `yaml:"resolution"`
}
type pnpmSnapshot struct {
Dependencies map[string]string `yaml:"dependencies"`
}
func parsePnpmLock(
data []byte,
graph *types.DependencyGraph,
rootPURL string,
directNames map[string]bool,
) {
var lock pnpmLockfile
if err := yaml.Unmarshal(data, &lock); err != nil {
return
}
for key, pkgEntry := range lock.Packages {
name, version := splitPnpmKey(key)
if name == "" {
continue
}
isDirect := directNames[name]
depth := 2
if isDirect {
depth = 1
}
purl := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(name), version)
pkg := types.Package{
Name: name,
Version: version,
Ecosystem: types.EcosystemNode,
PURL: purl,
Direct: isDirect,
DepthLevel: depth,
}
if pkgEntry.Resolution.Integrity != "" {
pkg.Checksums = append(pkg.Checksums, types.Checksum{
Algorithm: "SHA-512",
Value: pkgEntry.Resolution.Integrity,
})
}
graph.AddPackage(pkg)
if isDirect {
graph.AddEdge(rootPURL, purl)
}
}
for key, snap := range lock.Snapshots {
parentName, parentVersion := splitPnpmKey(key)
parentPURL := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(parentName), parentVersion)
for depName, depVersion := range snap.Dependencies {
childPURL := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(depName), depVersion)
graph.AddEdge(parentPURL, childPURL)
}
}
}
func parseFromPackageJSON(
pkg packageJSON,
graph *types.DependencyGraph,
rootPURL string,
) {
for name, version := range pkg.Dependencies {
cleanVersion := cleanNodeVersion(version)
purl := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(name), cleanVersion)
dep := types.Package{
Name: name,
Version: cleanVersion,
Ecosystem: types.EcosystemNode,
PURL: purl,
Direct: true,
DepthLevel: 1,
}
graph.AddPackage(dep)
graph.AddEdge(rootPURL, purl)
}
for name, version := range pkg.DevDependencies {
cleanVersion := cleanNodeVersion(version)
purl := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(name), cleanVersion)
dep := types.Package{
Name: name,
Version: cleanVersion,
Ecosystem: types.EcosystemNode,
PURL: purl,
Direct: true,
DepthLevel: 1,
}
graph.AddPackage(dep)
graph.AddEdge(rootPURL, purl)
}
}
func splitPnpmKey(key string) (string, string) {
atIdx := strings.LastIndex(key, "@")
if atIdx <= 0 {
return "", ""
}
return key[:atIdx], key[atIdx+1:]
}
func cleanNodeVersion(constraint string) string {
match := nodeSemverRe.FindString(constraint)
if match != "" {
return match
}
return strings.TrimLeft(constraint, "^~>=< ")
}
func encodePURLName(name string) string {
if strings.HasPrefix(name, "@") {
return strings.Replace(name, "@", "%40", 1)
}
return name
}