github.com/quay/claircore@v1.5.28/nodejs/packagescanner.go (about) 1 // Package nodejs contains components for interrogating nodejs packages in 2 // container layers. 3 package nodejs 4 5 import ( 6 "bufio" 7 "context" 8 "encoding/json" 9 "fmt" 10 "io/fs" 11 "path/filepath" 12 "runtime/trace" 13 "strings" 14 15 "github.com/quay/zlog" 16 17 "github.com/Masterminds/semver" 18 "github.com/quay/claircore" 19 "github.com/quay/claircore/indexer" 20 ) 21 22 const repository = "npm" 23 24 var ( 25 _ indexer.VersionedScanner = (*Scanner)(nil) 26 _ indexer.PackageScanner = (*Scanner)(nil) 27 _ indexer.DefaultRepoScanner = (*Scanner)(nil) 28 29 Repository = claircore.Repository{ 30 Name: repository, 31 URI: "https://www.npmjs.com/", 32 } 33 ) 34 35 // Scanner implements the scanner.PackageScanner interface. 36 // 37 // It looks for files that seem like package.json and looks at the 38 // metadata recorded there. 39 // 40 // The zero value is ready to use. 41 type Scanner struct{} 42 43 // Name implements scanner.VersionedScanner. 44 func (*Scanner) Name() string { return "nodejs" } 45 46 // Version implements scanner.VersionedScanner. 47 func (*Scanner) Version() string { return "2" } 48 49 // Kind implements scanner.VersionedScanner. 50 func (*Scanner) Kind() string { return "package" } 51 52 // packageJSON represents the fields of a package.json file 53 // useful for package scanning. 54 // 55 // See https://docs.npmjs.com/files/package.json/ for more details 56 // about the format of package.json files. 57 type packageJSON struct { 58 Name string `json:"name"` 59 Version string `json:"version"` 60 } 61 62 // Scan attempts to find package.json files and record the package 63 // information there. 64 // 65 // A return of (nil, nil) is expected if there's nothing found. 66 func (s *Scanner) Scan(ctx context.Context, layer *claircore.Layer) ([]*claircore.Package, error) { 67 defer trace.StartRegion(ctx, "Scanner.Scan").End() 68 trace.Log(ctx, "layer", layer.Hash.String()) 69 ctx = zlog.ContextWithValues(ctx, 70 "component", "nodejs/Scanner.Scan", 71 "version", s.Version(), 72 "layer", layer.Hash.String()) 73 zlog.Debug(ctx).Msg("start") 74 defer zlog.Debug(ctx).Msg("done") 75 if err := ctx.Err(); err != nil { 76 return nil, err 77 } 78 79 sys, err := layer.FS() 80 if err != nil { 81 return nil, fmt.Errorf("nodejs: unable to open layer: %w", err) 82 } 83 84 pkgs, err := packages(ctx, sys) 85 if err != nil { 86 return nil, fmt.Errorf("nodejs: failed to find packages: %w", err) 87 } 88 if len(pkgs) == 0 { 89 return nil, nil 90 } 91 92 ret := make([]*claircore.Package, 0, len(pkgs)) 93 var invalidPkgs []string 94 for _, p := range pkgs { 95 f, err := sys.Open(p) 96 if err != nil { 97 return nil, fmt.Errorf("nodejs: unable to open file %q: %w", p, err) 98 } 99 100 var pkgJSON packageJSON 101 err = json.NewDecoder(bufio.NewReader(f)).Decode(&pkgJSON) 102 if err != nil { 103 invalidPkgs = append(invalidPkgs, p) 104 continue 105 } 106 107 pkg := &claircore.Package{ 108 Name: pkgJSON.Name, 109 Version: pkgJSON.Version, 110 Kind: claircore.BINARY, 111 PackageDB: "nodejs:" + p, 112 Filepath: p, 113 RepositoryHint: repository, 114 } 115 if sv, err := semver.NewVersion(pkgJSON.Version); err == nil { 116 pkg.NormalizedVersion = claircore.FromSemver(sv) 117 } else { 118 zlog.Info(ctx). 119 Str("package", pkg.Name). 120 Str("version", pkg.Version). 121 Msg("invalid semantic version") 122 } 123 124 ret = append(ret, pkg) 125 } 126 127 if len(invalidPkgs) > 0 { 128 zlog.Debug(ctx).Strs("paths", invalidPkgs).Msg("unable to decode package.json, skipping") 129 } 130 131 return ret, nil 132 } 133 134 func packages(ctx context.Context, sys fs.FS) (out []string, err error) { 135 return out, fs.WalkDir(sys, ".", func(p string, d fs.DirEntry, err error) error { 136 ev := zlog.Debug(ctx). 137 Str("file", p) 138 var success bool 139 defer func() { 140 if !success { 141 ev.Discard().Send() 142 } 143 }() 144 switch { 145 case err != nil: 146 return err 147 case !d.Type().IsRegular(): 148 // Should we chase symlinks with the correct name? 149 return nil 150 case strings.HasPrefix(filepath.Base(p), ".wh."): 151 return nil 152 case !strings.Contains(p, "node_modules/"): 153 // Only bother with package.json files within node_modules/ directories. 154 // See https://docs.npmjs.com/cli/v7/configuring-npm/folders#node-modules 155 // for more information. 156 return nil 157 case strings.HasSuffix(p, "/package.json"): 158 ev = ev.Str("kind", "package.json") 159 default: 160 return nil 161 } 162 ev.Msg("found package") 163 success = true 164 out = append(out, p) 165 return nil 166 }) 167 } 168 169 // DefaultRepository implements [indexer.DefaultRepoScanner]. 170 func (*Scanner) DefaultRepository(_ context.Context) *claircore.Repository { 171 return &Repository 172 }