@@ -3,31 +3,22 @@ import path from 'node:path'
33
44import micromatch from 'micromatch'
55
6- import { defaultEntryFile , isAddressedByFile } from './scripts/build-tool.mts'
7- import { findSocketYmlSync } from '../../utils/config.mts'
6+ import { isAddressedByFile } from './scripts/build-tool.mts'
87import { realpathOrResolved } from '../../utils/fs.mts'
98import { globWithGitIgnore } from '../../utils/glob.mts'
109import { excludePathToScanIgnores } from '../scan/exclude-paths.mts'
1110
1211import type { BuildTool } from './scripts/build-tool.mts'
1312import type { SocketFactsSbomProject } from './scripts/facts.mts'
1413import type { SocketJson } from '../../utils/socket-json.mts'
14+ import type { SocketYml } from '@socketsecurity/config'
1515
1616const BUILD_TOOLS : BuildTool [ ] = [ 'gradle' , 'maven' , 'sbt' ]
1717
18- // In the pattern, not the walk, whose ignores (--exclude-paths, .gitignore)
19- // stay case-sensitive. Assumes every letter in the pattern is a literal.
20- function foldCase ( pattern : string ) : string {
21- return pattern . replace (
22- / [ a - z ] / gi,
23- c => `[${ c . toLowerCase ( ) } ${ c . toUpperCase ( ) } ]` ,
24- )
25- }
26-
27- // One glob-suffix set per ecosystem's build-descriptor marker(s); `settings.gradle(.kts)`
18+ // One glob set per ecosystem's build-descriptor marker(s); `settings.gradle(.kts)`
2819// covers Kotlin-DSL roots that have no root `build.gradle`. Maven mirrors the
29- // `pomxml` pattern of the API's supported files, case-insensitively as a scan
30- // matches it, so every POM a scan uploads is also a build root here.
20+ // `pomxml` pattern of the API's supported files, so every POM a scan uploads is
21+ // also a build root here.
3122const MARKERS_BY_TOOL : Record < BuildTool , string [ ] > = {
3223 __proto__ : null ,
3324 gradle : [
@@ -36,18 +27,21 @@ const MARKERS_BY_TOOL: Record<BuildTool, string[]> = {
3627 'settings.gradle' ,
3728 'settings.gradle.kts' ,
3829 ] ,
39- maven : [ foldCase ( '{*-*.,}pom{.xml,}' ) ] ,
30+ maven : [ '{*-*.,}pom{.xml,}' ] ,
4031 sbt : [ 'build.sbt' ] ,
4132} as unknown as Record < BuildTool , string [ ] >
4233
43- const MARKER_MATCHERS_BY_TOOL = Object . fromEntries (
44- BUILD_TOOLS . map ( tool => [
45- tool ,
46- MARKERS_BY_TOOL [ tool ] . map ( marker =>
47- micromatch . matcher ( marker , { dot : true } ) ,
48- ) ,
49- ] ) ,
50- ) as Record < BuildTool , Array < ( basename : string ) => boolean > >
34+ // Case-insensitive for POMs, as a scan matches them; Gradle and sbt look for
35+ // their build files by exact name.
36+ const MARKER_MATCHERS_BY_TOOL = new Map (
37+ BUILD_TOOLS . map ( tool => {
38+ const options = { dot : true , nocase : tool === 'maven' }
39+ const matchers = MARKERS_BY_TOOL [ tool ] . map ( marker =>
40+ micromatch . matcher ( marker , options ) ,
41+ )
42+ return [ tool , ( basename : string ) => matchers . some ( m => m ( basename ) ) ]
43+ } ) ,
44+ )
5145
5246// `entryFile` (a basename in `dir`) is set only for a tool that addresses its
5347// builds by file, where one directory can hold several builds.
@@ -61,9 +55,7 @@ function markerTool(
6155 tools : readonly BuildTool [ ] ,
6256 basename : string ,
6357) : BuildTool | undefined {
64- return tools . find ( tool =>
65- MARKER_MATCHERS_BY_TOOL [ tool ] . some ( matches => matches ( basename ) ) ,
66- )
58+ return tools . find ( tool => MARKER_MATCHERS_BY_TOOL . get ( tool ) ?.( basename ) )
6759}
6860
6961// A disabled ecosystem is skipped entirely by the glob scan below, so a
@@ -123,33 +115,32 @@ export function isBuildRootCovered(
123115 tool : BuildTool ,
124116 root : BuildRoot ,
125117) : boolean {
126- const entryFile = root . entryFile ?? defaultEntryFile ( tool )
127- if ( entryFile ) {
128- return covered . has ( path . join ( root . dir , entryFile ) )
118+ if ( root . entryFile ) {
119+ return covered . has ( buildRootPath ( root ) )
129120 }
130121 return MARKERS_BY_TOOL [ tool ] . some ( marker =>
131122 covered . has ( path . join ( root . dir , marker ) ) ,
132123 )
133124}
134125
135- function sortByDepthThenPath (
136- roots : readonly BuildRoot [ ] ,
126+ function compareStrings ( a : string , b : string ) : number {
127+ return a < b ? - 1 : a > b ? 1 : 0
128+ }
129+
130+ export function sortByDepthThenPath < T extends BuildRoot > (
131+ roots : readonly T [ ] ,
137132 cwd : string ,
138- ) : BuildRoot [ ] {
133+ ) : T [ ] {
139134 return [ ...roots ] . sort ( ( a , b ) => {
140135 const relA = path . relative ( cwd , a . dir )
141136 const relB = path . relative ( cwd , b . dir )
142137 const depthA = relA === '' ? 0 : relA . split ( path . sep ) . length
143138 const depthB = relB === '' ? 0 : relB . split ( path . sep ) . length
144- if ( depthA !== depthB ) {
145- return depthA - depthB
146- }
147- if ( relA !== relB ) {
148- return relA < relB ? - 1 : 1
149- }
150- const entryA = a . entryFile ?? ''
151- const entryB = b . entryFile ?? ''
152- return entryA < entryB ? - 1 : entryA > entryB ? 1 : 0
139+ return (
140+ depthA - depthB ||
141+ compareStrings ( relA , relB ) ||
142+ compareStrings ( a . entryFile ?? '' , b . entryFile ?? '' )
143+ )
153144 } )
154145}
155146
@@ -159,9 +150,13 @@ export async function findBuildToolCandidates({
159150 cwd,
160151 excludePaths,
161152 sockJson,
153+ socketConfig,
162154} : {
163155 cwd : string
164156 excludePaths ?: string [ ] | undefined
157+ // The socket.yml a scan applies to its upload, so a build file a scan would
158+ // not upload is not built either.
159+ socketConfig ?: SocketYml | undefined
165160 sockJson : SocketJson
166161} ) : Promise < Map < BuildTool , BuildRoot [ ] > > {
167162 const enabledTools = BUILD_TOOLS . filter (
@@ -172,20 +167,17 @@ export async function findBuildToolCandidates({
172167 return result
173168 }
174169
175- const patterns = enabledTools . flatMap ( tool =>
176- MARKERS_BY_TOOL [ tool ] . map ( marker => `**/${ marker } ` ) ,
177- )
178170 const additionalIgnores = ( excludePaths ?? [ ] ) . flatMap (
179171 excludePathToScanIgnores ,
180172 )
181- // The same socket.yml a scan applies to its upload, so a build file a scan
182- // would not upload is not built either.
183- const socketYml = findSocketYmlSync ( cwd )
184- const hits = await globWithGitIgnore ( patterns , {
173+ // Markers are matched in the filter, after the ignores, so --exclude-paths
174+ // and .gitignore stay case-sensitive as they are for a scan.
175+ const hits = await globWithGitIgnore ( [ '**/*' ] , {
185176 absolute : true ,
186177 additionalIgnores,
187178 cwd,
188- socketConfig : socketYml . ok ? socketYml . data ?. parsed : undefined ,
179+ filter : hit => ! ! markerTool ( enabledTools , path . basename ( hit ) ) ,
180+ socketConfig,
189181 } )
190182
191183 const rootsByTool = new Map < BuildTool , Map < string , BuildRoot > > (
@@ -197,14 +189,12 @@ export async function findBuildToolCandidates({
197189 return {
198190 basename,
199191 dir : await realpathOrResolved ( path . dirname ( hit ) ) ,
200- tool : markerTool ( enabledTools , basename ) ,
192+ // Set: the glob filter kept only marker hits.
193+ tool : markerTool ( enabledTools , basename ) as BuildTool ,
201194 }
202195 } ) ,
203196 )
204197 for ( const { basename, dir, tool } of resolvedHits ) {
205- if ( ! tool ) {
206- continue
207- }
208198 const root = {
209199 dir,
210200 entryFile : isAddressedByFile ( tool ) ? basename : undefined ,
0 commit comments