@@ -9,24 +9,25 @@ import {getCustomPath, invokeCommand } from "../../src/tools.js"
99let clock
1010
1111async function sharedComponentAnalysisTestFlow ( testCase , usePipDepTreeUtility ) {
12- // load the expected list for tsharedComponentAnalysisTestFlowhe scenario
13- let expectedSbom = fs . readFileSync ( `test/providers/tst_manifests/pip/${ testCase } /expected_component_sbom.json` ) . toString ( ) . trim ( )
14- expectedSbom = JSON . stringify ( JSON . parse ( expectedSbom ) )
15- // invoke sut stack analysis for scenario manifest
12+ // load the expected list for the scenario
13+ let expectedSbom = JSON . parse ( fs . readFileSync ( `test/providers/tst_manifests/pip/${ testCase } /expected_component_sbom.json` ) . toString ( ) . trim ( ) )
14+ // invoke sut component analysis for scenario manifest
1615 let opts = { TRUSTIFY_DA_PIP_USE_DEP_TREE : usePipDepTreeUtility . toString ( ) }
1716 let providedDatForComponent = await pythonPip . provideComponent ( `test/providers/tst_manifests/pip/${ testCase } /requirements.txt` , opts )
17+ let actualSbom = JSON . parse ( providedDatForComponent . content )
18+ // Strip hashes before comparison — they are platform-dependent.
19+ // Hash correctness is verified by the dedicated SHA-256 tests below.
20+ for ( let c of expectedSbom . components ) { delete c . hashes }
21+ for ( let c of actualSbom . components ) { delete c . hashes }
1822 // verify returned data matches expectation
19- expect ( providedDatForComponent ) . to . deep . equal ( {
20- ecosystem : 'pip' ,
21- contentType : 'application/vnd.cyclonedx+json' ,
22- content : expectedSbom
23- } )
23+ expect ( providedDatForComponent . ecosystem ) . to . equal ( 'pip' )
24+ expect ( providedDatForComponent . contentType ) . to . equal ( 'application/vnd.cyclonedx+json' )
25+ expect ( actualSbom ) . to . deep . equal ( expectedSbom )
2426}
2527
2628async function sharedStackAnalysisTestFlow ( testCase , usePipDepTreeUtility ) {
2729 // load the expected graph for the scenario
28- let expectedSbom = fs . readFileSync ( `test/providers/tst_manifests/pip/${ testCase } /expected_stack_sbom.json` ) . toString ( )
29- expectedSbom = JSON . stringify ( JSON . parse ( expectedSbom ) )
30+ let expectedSbom = JSON . parse ( fs . readFileSync ( `test/providers/tst_manifests/pip/${ testCase } /expected_stack_sbom.json` ) . toString ( ) )
3031 // invoke sut stack analysis for scenario manifest
3132 let pipPath = getCustomPath ( "pip3" ) ;
3233 try {
@@ -36,15 +37,15 @@ async function sharedStackAnalysisTestFlow(testCase, usePipDepTreeUtility) {
3637 }
3738 let opts = { TRUSTIFY_DA_PIP_USE_DEP_TREE : usePipDepTreeUtility . toString ( ) }
3839 let providedDataForStack = await pythonPip . provideStack ( `test/providers/tst_manifests/pip/${ testCase } /requirements.txt` , opts )
39- // new(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date
40-
41- // providedDataForStack.content = providedDataForStack.content.replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"","")
40+ let actualSbom = JSON . parse ( providedDataForStack . content )
41+ // Strip hashes before comparison — they are platform-dependent.
42+ // Hash correctness is verified by the dedicated SHA-256 tests below.
43+ for ( let c of expectedSbom . components ) { delete c . hashes }
44+ for ( let c of actualSbom . components ) { delete c . hashes }
4245 // verify returned data matches expectation
43- expect ( providedDataForStack ) . to . deep . equal ( {
44- ecosystem : 'pip' ,
45- contentType : 'application/vnd.cyclonedx+json' ,
46- content : expectedSbom
47- } )
46+ expect ( providedDataForStack . ecosystem ) . to . equal ( 'pip' )
47+ expect ( providedDataForStack . contentType ) . to . equal ( 'application/vnd.cyclonedx+json' )
48+ expect ( actualSbom ) . to . deep . equal ( expectedSbom )
4849}
4950
5051suite ( 'testing the python-pip data provider' , ( ) => {
@@ -153,3 +154,152 @@ suite('testing python-pip PEP 508 marker handling', () => {
153154 } ) . timeout ( 10000 )
154155
155156} ) . beforeAll ( ( ) => clock = useFakeTimers ( new Date ( '2023-10-01T00:00:00.000Z' ) ) ) . afterAll ( ( ) => clock . restore ( ) ) ;
157+
158+ suite ( 'testing python-pip SHA-256 hash extraction via pip inspect' , ( ) => {
159+ const hashTestCase = 'pip_requirements_txt_with_hashes'
160+
161+ const pipFreezeOutput = 'certifi==2023.7.22\nsix==1.16.0\n'
162+ const pipShowOutput =
163+ 'Name: certifi\nVersion: 2023.7.22\nSummary: Python package\nRequires: \nRequired-by: ' +
164+ '\n---\n' +
165+ 'Name: six\nVersion: 1.16.0\nSummary: Python 2 and 3 compatibility\nRequires: \nRequired-by: '
166+ const pipInspectOutput = JSON . stringify ( {
167+ version : "1" ,
168+ pip_version : "23.0" ,
169+ installed : [
170+ {
171+ metadata : { name : "certifi" , version : "2023.7.22" } ,
172+ download_info : { archive_info : { hashes : { sha256 : "539cc1d13202e33ca466e88b2807e29f4c13049d6d853261b21e0e8d461bbbf0" } } }
173+ } ,
174+ {
175+ metadata : { name : "six" , version : "1.16.0" } ,
176+ download_info : { archive_info : { hashes : { sha256 : "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" } } }
177+ }
178+ ]
179+ } )
180+
181+ /** Verifies that SHA-256 hashes from pip inspect are threaded into the component analysis SBOM. */
182+ test ( 'verify component analysis SBOM includes SHA-256 hashes from pip inspect' , async ( ) => {
183+ // Given: pip environment with freeze, show, and inspect data
184+ process . env [ 'TRUSTIFY_DA_PIP_FREEZE' ] = Buffer . from ( pipFreezeOutput ) . toString ( 'base64' )
185+ process . env [ 'TRUSTIFY_DA_PIP_SHOW' ] = Buffer . from ( pipShowOutput ) . toString ( 'base64' )
186+ process . env [ 'TRUSTIFY_DA_PIP_INSPECT' ] = Buffer . from ( pipInspectOutput ) . toString ( 'base64' )
187+
188+ try {
189+ // When: component analysis is run
190+ let expectedSbom = fs . readFileSync ( `test/providers/tst_manifests/pip/${ hashTestCase } /expected_component_sbom.json` ) . toString ( ) . trim ( )
191+ expectedSbom = JSON . stringify ( JSON . parse ( expectedSbom ) )
192+ let result = await pythonPip . provideComponent ( `test/providers/tst_manifests/pip/${ hashTestCase } /requirements.txt` , { } )
193+
194+ // Then: SBOM includes hashes matching the golden file
195+ expect ( result ) . to . deep . equal ( {
196+ ecosystem : 'pip' ,
197+ contentType : 'application/vnd.cyclonedx+json' ,
198+ content : expectedSbom
199+ } )
200+ } finally {
201+ delete process . env [ 'TRUSTIFY_DA_PIP_FREEZE' ]
202+ delete process . env [ 'TRUSTIFY_DA_PIP_SHOW' ]
203+ delete process . env [ 'TRUSTIFY_DA_PIP_INSPECT' ]
204+ }
205+ } ) . timeout ( 10000 )
206+
207+ /** Verifies that SHA-256 hashes from pip inspect are threaded into the stack analysis SBOM. */
208+ test ( 'verify stack analysis SBOM includes SHA-256 hashes from pip inspect' , async ( ) => {
209+ // Given: pip environment with freeze, show, and inspect data
210+ process . env [ 'TRUSTIFY_DA_PIP_FREEZE' ] = Buffer . from ( pipFreezeOutput ) . toString ( 'base64' )
211+ process . env [ 'TRUSTIFY_DA_PIP_SHOW' ] = Buffer . from ( pipShowOutput ) . toString ( 'base64' )
212+ process . env [ 'TRUSTIFY_DA_PIP_INSPECT' ] = Buffer . from ( pipInspectOutput ) . toString ( 'base64' )
213+
214+ try {
215+ // When: stack analysis is run
216+ let expectedSbom = fs . readFileSync ( `test/providers/tst_manifests/pip/${ hashTestCase } /expected_stack_sbom.json` ) . toString ( ) . trim ( )
217+ expectedSbom = JSON . stringify ( JSON . parse ( expectedSbom ) )
218+ let result = await pythonPip . provideStack ( `test/providers/tst_manifests/pip/${ hashTestCase } /requirements.txt` , { } )
219+
220+ // Then: SBOM includes hashes matching the golden file
221+ expect ( result ) . to . deep . equal ( {
222+ ecosystem : 'pip' ,
223+ contentType : 'application/vnd.cyclonedx+json' ,
224+ content : expectedSbom
225+ } )
226+ } finally {
227+ delete process . env [ 'TRUSTIFY_DA_PIP_FREEZE' ]
228+ delete process . env [ 'TRUSTIFY_DA_PIP_SHOW' ]
229+ delete process . env [ 'TRUSTIFY_DA_PIP_INSPECT' ]
230+ }
231+ } ) . timeout ( 10000 )
232+
233+ /** Verifies graceful degradation when pip inspect is unavailable (invalid output). */
234+ test ( 'verify SBOM generated without hashes when pip inspect output is invalid' , async ( ) => {
235+ // Given: pip environment with freeze and show but invalid inspect data
236+ process . env [ 'TRUSTIFY_DA_PIP_FREEZE' ] = Buffer . from ( pipFreezeOutput ) . toString ( 'base64' )
237+ process . env [ 'TRUSTIFY_DA_PIP_SHOW' ] = Buffer . from ( pipShowOutput ) . toString ( 'base64' )
238+ process . env [ 'TRUSTIFY_DA_PIP_INSPECT' ] = Buffer . from ( 'INVALID JSON' ) . toString ( 'base64' )
239+
240+ try {
241+ // When: component analysis is run
242+ let result = await pythonPip . provideComponent ( `test/providers/tst_manifests/pip/${ hashTestCase } /requirements.txt` , { } )
243+ let sbom = JSON . parse ( result . content )
244+
245+ // Then: SBOM is generated without hashes
246+ expect ( sbom . components ) . to . have . lengthOf ( 2 )
247+ for ( let component of sbom . components ) {
248+ expect ( component ) . to . not . have . property ( 'hashes' )
249+ }
250+ } finally {
251+ delete process . env [ 'TRUSTIFY_DA_PIP_FREEZE' ]
252+ delete process . env [ 'TRUSTIFY_DA_PIP_SHOW' ]
253+ delete process . env [ 'TRUSTIFY_DA_PIP_INSPECT' ]
254+ }
255+ } ) . timeout ( 10000 )
256+
257+ /** Verifies that pip inspect is skipped when freeze/show env vars are set without inspect. */
258+ test ( 'verify SBOM generated without hashes when pip inspect env var is not set' , async ( ) => {
259+ // Given: pip environment with freeze and show but NO inspect override
260+ process . env [ 'TRUSTIFY_DA_PIP_FREEZE' ] = Buffer . from ( pipFreezeOutput ) . toString ( 'base64' )
261+ process . env [ 'TRUSTIFY_DA_PIP_SHOW' ] = Buffer . from ( pipShowOutput ) . toString ( 'base64' )
262+
263+ try {
264+ // When: component analysis is run
265+ let result = await pythonPip . provideComponent ( `test/providers/tst_manifests/pip/${ hashTestCase } /requirements.txt` , { } )
266+ let sbom = JSON . parse ( result . content )
267+
268+ // Then: SBOM is generated without hashes (pip inspect skipped in env var mode)
269+ expect ( sbom . components ) . to . have . lengthOf ( 2 )
270+ for ( let component of sbom . components ) {
271+ expect ( component ) . to . not . have . property ( 'hashes' )
272+ }
273+ } finally {
274+ delete process . env [ 'TRUSTIFY_DA_PIP_FREEZE' ]
275+ delete process . env [ 'TRUSTIFY_DA_PIP_SHOW' ]
276+ }
277+ } ) . timeout ( 10000 )
278+
279+ } ) . beforeAll ( ( ) => clock = useFakeTimers ( new Date ( '2023-10-01T00:00:00.000Z' ) ) ) . afterAll ( ( ) => clock . restore ( ) ) ;
280+
281+ suite ( 'testing python-pip SHA-256 hashes with real pip inspect' , ( ) => {
282+ const testCase = 'pip_requirements_txt_with_hashes'
283+
284+ /** Verifies SBOM components include SHA-256 hashes from real pip inspect. */
285+ test ( 'SBOM components include SHA-256 hashes from pip inspect' , async ( ) => {
286+ // Given: packages installed via real pip
287+ let pipPath = getCustomPath ( "pip3" ) ;
288+ invokeCommand ( pipPath , [ 'install' , '-r' , `test/providers/tst_manifests/pip/${ testCase } /requirements.txt` ] )
289+
290+ // When: stack analysis is run (uses real pip freeze, pip show, and pip inspect)
291+ let result = await pythonPip . provideStack ( `test/providers/tst_manifests/pip/${ testCase } /requirements.txt` , { } )
292+ let sbom = JSON . parse ( result . content )
293+
294+ // Then: components with hashes have well-formed SHA-256 entries
295+ // Note: pip inspect may not include hashes for cached/pre-installed packages
296+ for ( let component of sbom . components ) {
297+ if ( component . hashes ) {
298+ expect ( component . hashes ) . to . be . an ( 'array' ) . with . lengthOf ( 1 )
299+ expect ( component . hashes [ 0 ] . alg ) . to . equal ( 'SHA-256' )
300+ expect ( component . hashes [ 0 ] . content ) . to . be . a ( 'string' ) . with . lengthOf ( 64 )
301+ }
302+ }
303+ } ) . timeout ( process . env . GITHUB_ACTIONS ? 30000 : 10000 )
304+
305+ } ) . beforeAll ( ( ) => clock = useFakeTimers ( new Date ( '2023-10-01T00:00:00.000Z' ) ) ) . afterAll ( ( ) => clock . restore ( ) ) ;
0 commit comments