Skip to content

Commit 335f974

Browse files
chris-rockBajusz15
andauthored
🐛 read package.json from root path (#6181)
* 🐛 also read root path of the node package, this allows us to work with unresolved packages as well * add tests --------- Co-authored-by: Máté Bajusz <mate@mondoo.com>
1 parent 699987d commit 335f974

File tree

2 files changed

+276
-15
lines changed

2 files changed

+276
-15
lines changed

providers/os/resources/npm.go

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,39 @@ func collectNpmPackagesInPaths(runtime *plugin.Runtime, fs afero.Fs, paths []str
7373
var transitivePackageList []*languages.Package
7474
evidenceFiles := []string{}
7575

76+
handler := func(nodeModulesPath string) {
77+
// Not found is an expected error and we handle that properly
78+
bom, err := collectNpmPackages(runtime, fs, nodeModulesPath)
79+
if err != nil {
80+
return
81+
}
82+
83+
root := bom.Root()
84+
if root != nil {
85+
directPackageList = append(directPackageList, root)
86+
}
87+
transitive := bom.Transitive()
88+
if transitive != nil {
89+
transitivePackageList = append(transitivePackageList, transitive...)
90+
}
91+
}
92+
7693
log.Debug().Msg("searching for npm packages in default locations")
7794
err := fsutil.WalkGlob(fs, paths, func(fs afero.Fs, walkPath string) error {
7895
afs := &afero.Afero{Fs: fs}
7996

97+
// check root directory
98+
handler(walkPath)
99+
100+
// if we have a lock file, we do not need to check for node_modules directory
101+
if hasLockfile(runtime, fs, walkPath) {
102+
return nil
103+
}
104+
80105
// we walk through the directories and check if there is a node_modules directory
81106
log.Debug().Str("path", walkPath).Msg("found npm package")
82107
nodeModulesPath := filepath.Join(walkPath, "node_modules")
108+
83109
files, err := afs.ReadDir(nodeModulesPath)
84110
if err != nil {
85111
// we ignore the error, it is expected that there is no node_modules directory
@@ -94,21 +120,7 @@ func collectNpmPackagesInPaths(runtime *plugin.Runtime, fs afero.Fs, paths []str
94120
}
95121

96122
log.Debug().Str("path", p).Msg("checking for package-lock.json or package.json file")
97-
98-
// Not found is an expected error and we handle that properly
99-
bom, err := collectNpmPackages(runtime, fs, filepath.Join(nodeModulesPath, p))
100-
if err != nil {
101-
continue
102-
}
103-
104-
root := bom.Root()
105-
if root != nil {
106-
directPackageList = append(directPackageList, root)
107-
}
108-
transitive := bom.Transitive()
109-
if transitive != nil {
110-
transitivePackageList = append(transitivePackageList, transitive...)
111-
}
123+
handler(filepath.Join(nodeModulesPath, p))
112124
}
113125
return nil
114126
})
@@ -118,6 +130,34 @@ func collectNpmPackagesInPaths(runtime *plugin.Runtime, fs afero.Fs, paths []str
118130
return directPackageList, transitivePackageList, evidenceFiles, nil
119131
}
120132

133+
// hasLockfile checks for the lock files
134+
func hasLockfile(runtime *plugin.Runtime, fs afero.Fs, path string) bool {
135+
// specific path was provided
136+
afs := &afero.Afero{Fs: fs}
137+
isDir, err := afs.IsDir(path)
138+
if err != nil {
139+
return false
140+
}
141+
142+
searchPaths := []string{}
143+
if isDir {
144+
// check if there is a package-lock.json or package.json file
145+
searchPaths = append(searchPaths, filepath.Join(path, "/package-lock.json"))
146+
} else if strings.HasSuffix(path, "package-lock.json") {
147+
searchPaths = append(searchPaths, path)
148+
}
149+
150+
// filter out non-existing files using the new slice package
151+
filteredSearchPath := []string{}
152+
for i := range searchPaths {
153+
exists, _ := afs.Exists(searchPaths[i])
154+
if exists {
155+
filteredSearchPath = append(filteredSearchPath, searchPaths[i])
156+
}
157+
}
158+
return len(filteredSearchPath) > 0
159+
}
160+
121161
func collectNpmPackages(runtime *plugin.Runtime, fs afero.Fs, path string) (languages.Bom, error) {
122162
// specific path was provided
123163
afs := &afero.Afero{Fs: fs}

providers/os/resources/npm_test.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
1515
"go.mondoo.com/cnquery/v12/providers-sdk/v1/plugin"
1616
"go.mondoo.com/cnquery/v12/providers/os/connection/fs"
17+
"go.mondoo.com/cnquery/v12/providers/os/resources/languages"
1718
"go.mondoo.com/cnquery/v12/types"
1819
"go.mondoo.com/cnquery/v12/utils/syncx"
1920
)
@@ -96,3 +97,223 @@ func (p *providerCallbacks) GetRecording(req *plugin.DataReq) (*plugin.ResourceD
9697
func (p *providerCallbacks) Collect(req *plugin.DataRes) error {
9798
return nil
9899
}
100+
101+
func TestCollectNpmPackagesInPaths(t *testing.T) {
102+
tests := []struct {
103+
name string
104+
setup func(*testing.T, afero.Fs) string
105+
validate func(*testing.T, []*languages.Package, []*languages.Package, []string)
106+
}{
107+
{
108+
name: "root package.json, no lock file, no node_modules",
109+
setup: func(t *testing.T, mockFS afero.Fs) string {
110+
// Create a test project directory with package.json at root (no lock file, no node_modules)
111+
testProjectPath := "/app"
112+
err := mockFS.MkdirAll(testProjectPath, 0o755)
113+
require.NoError(t, err)
114+
115+
// Create a simple package.json at root
116+
rootPackageJSON := `{
117+
"name": "test-app",
118+
"version": "1.0.0",
119+
"dependencies": {
120+
"lodash": "^4.17.21"
121+
}
122+
}`
123+
err = afero.WriteFile(mockFS, filepath.Join(testProjectPath, "package.json"), []byte(rootPackageJSON), 0o644)
124+
require.NoError(t, err)
125+
return testProjectPath
126+
},
127+
validate: func(t *testing.T, direct, transitive []*languages.Package, evidenceFiles []string) {
128+
require.Empty(t, evidenceFiles)
129+
require.Greater(t, len(direct), 0, "should find at least the root package")
130+
131+
foundRoot := false
132+
for _, pkg := range direct {
133+
if pkg.Name == "test-app" {
134+
foundRoot = true
135+
require.Equal(t, "1.0.0", pkg.Version)
136+
break
137+
}
138+
}
139+
require.True(t, foundRoot, "should find root package 'test-app'")
140+
},
141+
},
142+
{
143+
name: "with lock file",
144+
setup: func(t *testing.T, mockFS afero.Fs) string {
145+
// Create a test project with package-lock.json at root
146+
testProjectPath := "/app"
147+
err := mockFS.MkdirAll(testProjectPath, 0o755)
148+
require.NoError(t, err)
149+
150+
// Create package.json at root
151+
rootPackageJSON := `{
152+
"name": "test-app",
153+
"version": "1.0.0"
154+
}`
155+
err = afero.WriteFile(mockFS, filepath.Join(testProjectPath, "package.json"), []byte(rootPackageJSON), 0o644)
156+
require.NoError(t, err)
157+
158+
// Create package-lock.json at root (this should cause node_modules to be skipped)
159+
packageLockJSON := `{
160+
"name": "test-app",
161+
"version": "1.0.0",
162+
"lockfileVersion": 2
163+
}`
164+
err = afero.WriteFile(mockFS, filepath.Join(testProjectPath, "package-lock.json"), []byte(packageLockJSON), 0o644)
165+
require.NoError(t, err)
166+
167+
// Create node_modules directory with a package (should be skipped due to lock file)
168+
err = mockFS.MkdirAll(filepath.Join(testProjectPath, "node_modules", "some-package"), 0o755)
169+
require.NoError(t, err)
170+
nodeModulesPackageJSON := `{
171+
"name": "some-package",
172+
"version": "2.0.0"
173+
}`
174+
err = afero.WriteFile(mockFS, filepath.Join(testProjectPath, "node_modules", "some-package", "package.json"), []byte(nodeModulesPackageJSON), 0o644)
175+
require.NoError(t, err)
176+
return testProjectPath
177+
},
178+
validate: func(t *testing.T, direct, transitive []*languages.Package, evidenceFiles []string) {
179+
require.Empty(t, evidenceFiles)
180+
require.Greater(t, len(direct), 0, "should find at least the root package")
181+
182+
// Should NOT find the node_modules package (because lock file exists)
183+
foundNodeModulesPackage := false
184+
for _, pkg := range direct {
185+
if pkg.Name == "some-package" {
186+
foundNodeModulesPackage = true
187+
break
188+
}
189+
}
190+
require.False(t, foundNodeModulesPackage, "should not find node_modules package when lock file exists")
191+
},
192+
},
193+
{
194+
name: "root and node_modules, no lock file",
195+
setup: func(t *testing.T, mockFS afero.Fs) string {
196+
// Create a test project with both root package.json and node_modules
197+
testProjectPath := "/app"
198+
err := mockFS.MkdirAll(testProjectPath, 0o755)
199+
require.NoError(t, err)
200+
201+
// Create package.json at root (no lock file)
202+
rootPackageJSON := `{
203+
"name": "test-app",
204+
"version": "1.0.0"
205+
}`
206+
err = afero.WriteFile(mockFS, filepath.Join(testProjectPath, "package.json"), []byte(rootPackageJSON), 0o644)
207+
require.NoError(t, err)
208+
209+
// Create node_modules directory with a package (should be checked since no lock file)
210+
err = mockFS.MkdirAll(filepath.Join(testProjectPath, "node_modules", "lodash"), 0o755)
211+
require.NoError(t, err)
212+
213+
// Read test data for a real package
214+
yoPkg, err := os.ReadFile(filepath.Join("packages", "testdata", "yo_package.json"))
215+
require.NoError(t, err)
216+
err = afero.WriteFile(mockFS, filepath.Join(testProjectPath, "node_modules", "lodash", "package.json"), yoPkg, 0o644)
217+
require.NoError(t, err)
218+
return testProjectPath
219+
},
220+
validate: func(t *testing.T, direct, transitive []*languages.Package, evidenceFiles []string) {
221+
require.Empty(t, evidenceFiles)
222+
// Should find packages from both root and node_modules
223+
require.Greater(t, len(direct)+len(transitive), 0, "should find packages from root and/or node_modules")
224+
},
225+
},
226+
}
227+
228+
for _, tt := range tests {
229+
t.Run(tt.name, func(t *testing.T) {
230+
mockFS := afero.NewMemMapFs()
231+
testProjectPath := tt.setup(t, mockFS)
232+
233+
conn, err := fs.NewFileSystemConnectionWithFs(0, &inventory.Config{}, &inventory.Asset{}, "", nil, mockFS)
234+
require.NoError(t, err)
235+
236+
r := &plugin.Runtime{
237+
Resources: &syncx.Map[plugin.Resource]{},
238+
Connection: conn,
239+
Callback: &providerCallbacks{},
240+
}
241+
242+
direct, transitive, evidenceFiles, err := collectNpmPackagesInPaths(r, conn.FileSystem(), []string{testProjectPath})
243+
require.NoError(t, err)
244+
245+
tt.validate(t, direct, transitive, evidenceFiles)
246+
})
247+
}
248+
}
249+
250+
// TestCollectNpmPackagesInPaths_filtering tests the filtering logic for non-existent paths
251+
func TestCollectNpmPackagesInPaths_skipsNoneExistentPaths(t *testing.T) {
252+
mockFS := afero.NewMemMapFs()
253+
254+
// Create a valid path with package.json
255+
validPath := "/app1"
256+
err := mockFS.MkdirAll(validPath, 0o755)
257+
require.NoError(t, err)
258+
259+
rootPackageJSON := `{
260+
"name": "test-app-1",
261+
"version": "1.0.0"
262+
}`
263+
err = afero.WriteFile(mockFS, filepath.Join(validPath, "package.json"), []byte(rootPackageJSON), 0o644)
264+
require.NoError(t, err)
265+
266+
// Create another valid path with package.json
267+
validPath2 := "/app2"
268+
err = mockFS.MkdirAll(validPath2, 0o755)
269+
require.NoError(t, err)
270+
271+
rootPackageJSON2 := `{
272+
"name": "test-app-2",
273+
"version": "2.0.0"
274+
}`
275+
err = afero.WriteFile(mockFS, filepath.Join(validPath2, "package.json"), []byte(rootPackageJSON2), 0o644)
276+
require.NoError(t, err)
277+
278+
conn, err := fs.NewFileSystemConnectionWithFs(0, &inventory.Config{}, &inventory.Asset{}, "", nil, mockFS)
279+
require.NoError(t, err)
280+
281+
r := &plugin.Runtime{
282+
Resources: &syncx.Map[plugin.Resource]{},
283+
Connection: conn,
284+
Callback: &providerCallbacks{},
285+
}
286+
287+
// Test with multiple paths: some exist, some don't
288+
// This tests the filtering logic in collectNpmPackages and hasLockfile
289+
paths := []string{
290+
validPath, // exists
291+
"/nonexistent/path1", // doesn't exist
292+
validPath2, // exists
293+
"/nonexistent/path2", // doesn't exist
294+
"/another/missing", // doesn't exist
295+
}
296+
297+
direct, _, evidenceFiles, err := collectNpmPackagesInPaths(r, conn.FileSystem(), paths)
298+
require.NoError(t, err)
299+
require.Empty(t, evidenceFiles)
300+
301+
// Should find packages from existing paths only
302+
require.Greater(t, len(direct), 0, "should find packages from existing paths")
303+
304+
// Verify we found both valid packages
305+
foundApp1 := false
306+
foundApp2 := false
307+
for _, pkg := range direct {
308+
if pkg.Name == "test-app-1" {
309+
foundApp1 = true
310+
require.Equal(t, "1.0.0", pkg.Version)
311+
}
312+
if pkg.Name == "test-app-2" {
313+
foundApp2 = true
314+
require.Equal(t, "2.0.0", pkg.Version)
315+
}
316+
}
317+
require.True(t, foundApp1, "should find test-app-1 from valid path")
318+
require.True(t, foundApp2, "should find test-app-2 from valid path")
319+
}

0 commit comments

Comments
 (0)