Skip to content

Commit 89944d7

Browse files
authored
Merge pull request #64 from RHEcosystemAppEng/match-manifest-versions
feat: add functionality of matching manifest versions for python' pip and golang' go modules. Jira Ticket: https://issues.redhat.com/browse/APPENG-2062
2 parents d8e7c45 + 4f93998 commit 89944d7

File tree

4 files changed

+123
-4
lines changed

4 files changed

+123
-4
lines changed

src/providers/golang_gomodules.js

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execSync } from "node:child_process"
33
import fs from 'node:fs'
44
import os from "node:os";
55
import {EOL} from "os";
6-
import { getCustomPath } from "../tools.js";
6+
import {getCustom, getCustomPath} from "../tools.js";
77
import path from 'node:path'
88
import Sbom from '../sbom.js'
99
import {PackageURL} from 'packageurl-js'
@@ -174,6 +174,93 @@ function enforceRemovingIgnoredDepsInCaseOfAutomaticVersionUpdate(ignoredDeps, s
174174
})
175175
}
176176

177+
/**
178+
*
179+
* @param {[string]} lines - array of lines of go.mod manifest
180+
* @param {string} goMod - content of go.mod manifest
181+
* @return {[string]} all dependencies from go.mod file as array
182+
*/
183+
function collectAllDepsFromManifest(lines, goMod) {
184+
let result
185+
// collect all deps that starts with require keyword
186+
187+
result = lines.filter((line) => line.trim().startsWith("require") && !line.includes("(")).map((dep) => dep.substring("require".length).trim())
188+
189+
190+
191+
// collect all deps that are inside `require` blocks
192+
let currentSegmentOfGoMod = goMod
193+
let requirePositionObject = decideRequireBlockIndex(currentSegmentOfGoMod)
194+
while(requirePositionObject.index > -1) {
195+
let depsInsideRequirementsBlock = currentSegmentOfGoMod.substring(requirePositionObject.index + requirePositionObject.startingOffeset).trim();
196+
let endOfBlockIndex = depsInsideRequirementsBlock.indexOf(")")
197+
let currentIndex = 0
198+
while(currentIndex < endOfBlockIndex)
199+
{
200+
let endOfLinePosition = depsInsideRequirementsBlock.indexOf(EOL, currentIndex);
201+
let dependency = depsInsideRequirementsBlock.substring(currentIndex, endOfLinePosition)
202+
result.push(dependency.trim())
203+
currentIndex = endOfLinePosition + 1
204+
}
205+
currentSegmentOfGoMod = currentSegmentOfGoMod.substring(endOfBlockIndex + 1).trim()
206+
requirePositionObject = decideRequireBlockIndex(currentSegmentOfGoMod)
207+
}
208+
209+
function decideRequireBlockIndex(goMod) {
210+
let object = {}
211+
let index = goMod.indexOf("require(")
212+
object.startingOffeset = "require(".length
213+
if (index === -1)
214+
{
215+
index = goMod.indexOf("require (")
216+
object.startingOffeset = "require (".length
217+
if(index === -1)
218+
{
219+
index = goMod.indexOf("require (")
220+
object.startingOffeset = "require (".length
221+
}
222+
}
223+
object.index = index
224+
return object
225+
}
226+
return result
227+
}
228+
229+
/**
230+
*
231+
* @param {string} rootElementName the rootElementName element of go mod graph, to compare only direct deps from go mod graph against go.mod manifest
232+
* @param{[string]} goModGraphOutputRows the goModGraphOutputRows from go mod graph' output
233+
* @param {string }manifest path to go.mod manifest on file system
234+
* @private
235+
*/
236+
function performManifestVersionsCheck(rootElementName, goModGraphOutputRows, manifest) {
237+
let goMod = fs.readFileSync(manifest).toString().trim()
238+
let lines = goMod.split(EOL);
239+
let comparisonLines = goModGraphOutputRows.filter((line)=> line.startsWith(rootElementName)).map((line)=> getChildVertexFromEdge(line))
240+
let manifestDeps = collectAllDepsFromManifest(lines,goMod)
241+
try {
242+
comparisonLines.forEach((dependency) => {
243+
let parts = dependency.split("@")
244+
let version = parts[1]
245+
let depName = parts[0]
246+
manifestDeps.forEach(dep => {
247+
let components = dep.trim().split(" ");
248+
let currentDepName = components[0]
249+
let currentVersion = components[1]
250+
if (currentDepName === depName) {
251+
if (currentVersion !== version) {
252+
throw new Error(`versions mismatch for dependency name ${depName}, manifest version=${currentVersion}, installed Version=${version}, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false`)
253+
}
254+
}
255+
})
256+
})
257+
}
258+
catch(error) {
259+
console.error("Can't continue with analysis")
260+
throw error
261+
}
262+
}
263+
177264
/**
178265
* Create SBOM json string for go Module.
179266
* @param {string} manifest - path for go.mod
@@ -201,6 +288,12 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
201288
let sbom = new Sbom();
202289
let rows = goGraphOutput.split(EOL);
203290
let root = getParentVertexFromEdge(rows[0])
291+
let matchManifestVersions = getCustom("MATCH_MANIFEST_VERSIONS","false");
292+
if(matchManifestVersions === "true") {
293+
{
294+
performManifestVersionsCheck(root, rows, manifest)
295+
}
296+
}
204297
let mainModule = toPurl(root, "@", undefined)
205298
sbom.addRoot(mainModule)
206299

src/providers/python_controller.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {execSync} from "node:child_process";
22
import fs from "node:fs";
33
import path from 'node:path';
44
import {EOL} from "os";
5+
import {getCustom} from "../tools.js";
56

67

78
/** @typedef {{name: string, version: string, dependencies: DependencyEntry[]}} DependencyEntry */
@@ -126,6 +127,7 @@ export default class Python_controller {
126127
}
127128
}).toString();
128129
let allPipShowDeps = pipShowOutput.split( EOL +"---" + EOL);
130+
let matchManifestVersions = getCustom("MATCH_MANIFEST_VERSIONS","true");
129131
let linesOfRequirements = fs.readFileSync(this.pathToRequirements).toString().split(EOL).filter( (line) => !line.startsWith("#")).map(line => line.trim())
130132
let CachedEnvironmentDeps = {}
131133
allPipShowDeps.forEach( (record) => {
@@ -135,6 +137,31 @@ export default class Python_controller {
135137
CachedEnvironmentDeps[dependencyName.replace("_","-")] = record
136138
})
137139
linesOfRequirements.forEach( (dep) => {
140+
// if matchManifestVersions setting is turned on , then
141+
if(matchManifestVersions === "true")
142+
{
143+
let dependencyName
144+
let manifestVersion
145+
let installedVersion
146+
let doubleEqualSignPosition
147+
if(dep.includes("=="))
148+
{
149+
doubleEqualSignPosition = dep.indexOf("==")
150+
manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim()
151+
if(manifestVersion.includes("#"))
152+
{
153+
let hashCharIndex = manifestVersion.indexOf("#");
154+
manifestVersion = manifestVersion.substring(0,hashCharIndex)
155+
}
156+
dependencyName = getDependencyName(dep)
157+
installedVersion = getDependencyVersion(CachedEnvironmentDeps[dependencyName.toLowerCase()])
158+
if(manifestVersion.trim() !== installedVersion.trim())
159+
{
160+
throw new Error(`Can't continue with analysis - versions mismatch for dependency name ${dependencyName}, manifest version=${manifestVersion}, installed Version=${installedVersion}, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false`)
161+
}
162+
163+
}
164+
}
138165
bringAllDependencies(dependencies,getDependencyName(dep),CachedEnvironmentDeps,includeTransitive)
139166
})
140167
dependencies.sort((dep1,dep2) =>{

test/providers/golang_gomodules.test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ suite('testing the golang-go-modules data provider', () => {
2929
let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/golang/${testCase}/expected_sbom_stack_analysis.json`,).toString()
3030
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
3131
// invoke sut stack analysis for scenario manifest
32-
3332
let providedDataForStack = await golangGoModules.provideStack(`test/providers/tst_manifests/golang/${testCase}/go.mod`)
3433
// new(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date
3534

test/providers/python_pip.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ suite('testing the python-pip data provider', () => {
6767

6868
}).beforeAll(() => clock = sinon.useFakeTimers(new Date('2023-10-01T00:00:00.000Z'))).afterAll(()=> clock.restore());
6969

70-
suite('testing the python-pip data provider', () => {
70+
suite('testing the python-pip data provider with virtual environment', () => {
7171
[
7272
"pip_requirements_virtual_env_txt_no_ignore",
7373
"pip_requirements_virtual_env_with_ignore"
@@ -93,7 +93,7 @@ suite('testing the python-pip data provider', () => {
9393
// content: expectedSbom
9494
// })
9595
// these test cases takes ~2500-2700 ms each pr >10000 in CI (for the first test-case)
96-
}).timeout(process.env.GITHUB_ACTIONS ? 30000 : 15000)
96+
}).timeout(process.env.GITHUB_ACTIONS ? 60000 : 30000)
9797

9898

9999
})

0 commit comments

Comments
 (0)