diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java index 72bd2b0d..bfe021c8 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java @@ -210,6 +210,11 @@ private static String extractDepFull(String requirementLine) { } private String[] splitToNameVersion(String nameVersion) { + // Strip PEP 508 environment markers (everything after ";") + int markerIndex = nameVersion.indexOf(';'); + if (markerIndex != -1) { + nameVersion = nameVersion.substring(0, markerIndex).trim(); + } String[] result; if (nameVersion.matches( "[a-zA-Z0-9-_()]+={2}[0-9]{1,4}[.][0-9]{1,4}(([.][0-9]{1,4})|([.][a-zA-Z0-9]+)|([a-zA-Z0-9]+)|([.][a-zA-Z0-9]+[.][a-z-A-Z0-9]+))?")) { diff --git a/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java b/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java index cc367bc1..67d92569 100644 --- a/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java +++ b/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java @@ -388,31 +388,24 @@ public static String getDependencyName(String dep) { requirement = requirement.substring(0, extrasStart) + requirement.substring(extrasEnd + 1); } } - int rightTriangleBracket = requirement.indexOf(">"); - int leftTriangleBracket = requirement.indexOf("<"); - int equalsSign = requirement.indexOf("="); - int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign); + // Find the first PEP 508 version operator character (~=, !=, ==, >=, <=, >, <, ===) + int firstOperatorChar = -1; + for (int i = 0; i < requirement.length(); i++) { + char c = requirement.charAt(i); + if (c == '>' || c == '<' || c == '=' || c == '~' || c == '!') { + firstOperatorChar = i; + break; + } + } String depName; - if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) { + if (firstOperatorChar == -1) { depName = requirement; } else { - depName = requirement.substring(0, minimumIndex); + depName = requirement.substring(0, firstOperatorChar); } return depName.trim(); } - private static int getFirstSign( - int rightTriangleBracket, int leftTriangleBracket, int equalsSign) { - rightTriangleBracket = rightTriangleBracket == -1 ? 999 : rightTriangleBracket; - leftTriangleBracket = leftTriangleBracket == -1 ? 999 : leftTriangleBracket; - equalsSign = equalsSign == -1 ? 999 : equalsSign; - return equalsSign < leftTriangleBracket && equalsSign < rightTriangleBracket - ? equalsSign - : (leftTriangleBracket < equalsSign && leftTriangleBracket < rightTriangleBracket - ? leftTriangleBracket - : rightTriangleBracket); - } - static List splitPipShowLines(String pipShowOutput) { return Arrays.stream( pipShowOutput.split(System.lineSeparator() + "---" + System.lineSeparator())) diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java b/src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java index 2f6cce69..c9580e54 100644 --- a/src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java +++ b/src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java @@ -267,6 +267,39 @@ void Test_The_ProvideComponent_Path_Should_Throw_Exception() { .isThrownBy(() -> new PythonPipProvider(Path.of(".")).provideComponent()); } + @Test + void getIgnoredDependencies_strips_environment_markers() throws IOException { + Path tempFile = Files.createTempFile("requirements", ".txt"); + try { + Files.writeString( + tempFile, + String.join( + System.lineSeparator(), + "requests==2.25.1 ; python_version >= \"3.6\" #trustify-da-ignore", + "idna==2.10 ; python_version >= \"3.6\" # trustify-da-ignore", + "six==1.16.0 ; python_version < \"3.0\" or python_version >= \"3.3\"" + + " #trustify-da-ignore", + "chardet==4.0.0 ; python_version >= \"3.6\" and sys_platform == \"linux\"" + + " #trustify-da-ignore", + "flask==2.0.3")); + var provider = new PythonPipProvider(tempFile); + var ignored = provider.getIgnoredDependencies(Files.readString(tempFile)); + var ignoredMap = + ignored.stream() + .collect( + java.util.stream.Collectors.toMap( + com.github.packageurl.PackageURL::getName, + com.github.packageurl.PackageURL::getVersion)); + assertThat(ignoredMap).containsEntry("requests", "2.25.1"); + assertThat(ignoredMap).containsEntry("idna", "2.10"); + assertThat(ignoredMap).containsEntry("six", "1.16.0"); + assertThat(ignoredMap).containsEntry("chardet", "4.0.0"); + assertThat(ignoredMap).doesNotContainKey("flask"); + } finally { + Files.deleteIfExists(tempFile); + } + } + private String dropIgnored(String s) { return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); } diff --git a/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java b/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java index ee63d5d1..4aa98d2c 100644 --- a/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java +++ b/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java @@ -301,6 +301,16 @@ void get_Dependency_Name_with_markers() { PythonControllerRealEnv.getDependencyName("certifi==2023.7.22 ; python_version >= \"3\"")); } + /** Verifies getDependencyName handles compatibility (~=) and exclusion (!=) operators. */ + @Test + void get_Dependency_Name_with_compatibility_and_exclusion_operators() { + assertEquals("urllib3", PythonControllerRealEnv.getDependencyName("urllib3~=1.26.0")); + assertEquals("click", PythonControllerRealEnv.getDependencyName("click!=7.1.1")); + assertEquals("certifi", PythonControllerRealEnv.getDependencyName("certifi>=2021.0.0")); + assertEquals("package", PythonControllerRealEnv.getDependencyName("package~=2.0")); + assertEquals("package", PythonControllerRealEnv.getDependencyName("package!=1.0.0")); + } + /** Verifies getDependencyName strips PEP 508 extras from requirements. */ @Test void get_Dependency_Name_with_extras() { @@ -313,6 +323,17 @@ void get_Dependency_Name_with_extras() { "package[extra1]>=1.0 ; python_version >= \"3.8\"")); } + /** Verifies getDependencyName handles extras combined with special version operators. */ + @Test + void get_Dependency_Name_with_extras_and_special_operators() { + assertEquals( + "requests", PythonControllerRealEnv.getDependencyName("requests[security,socks]==2.25.1")); + assertEquals("httpx", PythonControllerRealEnv.getDependencyName("httpx [http2] >=0.23.0")); + assertEquals( + "package", + PythonControllerRealEnv.getDependencyName("package[extra]~=1.0 ; python_version >= \"3\"")); + } + @Test void automaticallyInstallPackageOnEnvironment() { assertFalse(pythonControllerRealEnv.automaticallyInstallPackageOnEnvironment());