1919
2020import org .apache .commons .collections4 .MultiValuedMap ;
2121import org .apache .commons .collections4 .multimap .HashSetValuedHashMap ;
22- import org .apache .commons .io .IOUtils ;
2322import org .apache .commons .lang3 .StringUtils ;
2423import org .owasp .dependencycheck .Engine ;
2524import org .owasp .dependencycheck .analyzer .exception .AnalysisException ;
26- import org .owasp .dependencycheck .analyzer .exception .SearchException ;
2725import org .owasp .dependencycheck .analyzer .exception .UnexpectedAnalysisException ;
2826import org .owasp .dependencycheck .data .nodeaudit .Advisory ;
29- import org .owasp .dependencycheck .data .nodeaudit .NpmPayloadBuilder ;
3027import org .owasp .dependencycheck .dependency .Dependency ;
3128import org .owasp .dependencycheck .exception .InitializationException ;
3229import org .owasp .dependencycheck .utils .FileFilterBuilder ;
3330import org .owasp .dependencycheck .utils .Settings ;
34- import org .owasp .dependencycheck .utils .URLConnectionFailureException ;
3531import org .owasp .dependencycheck .utils .processing .ProcessReader ;
32+ import org .semver4j .Semver ;
33+ import org .semver4j .SemverException ;
3634import org .slf4j .Logger ;
3735import org .slf4j .LoggerFactory ;
3836import us .springett .parsers .cpe .exceptions .CpeValidationException ;
3937
4038import javax .annotation .concurrent .ThreadSafe ;
41- import jakarta .json .Json ;
42- import jakarta .json .JsonException ;
43- import jakarta .json .JsonObject ;
44- import jakarta .json .JsonReader ;
4539import java .io .File ;
4640import java .io .FileFilter ;
4741import java .io .IOException ;
4842import java .nio .charset .StandardCharsets ;
4943import java .nio .file .Files ;
5044import java .util .ArrayList ;
51- import java .util .Arrays ;
5245import java .util .List ;
5346
5447@ ThreadSafe
55- public class YarnAuditAnalyzer extends AbstractNpmAnalyzer {
48+ public abstract class AbstractYarnAuditAnalyzer extends AbstractNpmAnalyzer {
5649
57- /**
58- * The logger.
59- */
60- private static final Logger LOGGER = LoggerFactory .getLogger (YarnAuditAnalyzer .class );
50+ private static final Logger LOGGER = LoggerFactory .getLogger (AbstractYarnAuditAnalyzer .class );
51+ protected static final int YARN_CLASSIC_MAJOR_VERSION = 1 ;
6152
6253 /**
6354 * The file name to scan.
@@ -71,23 +62,39 @@ public class YarnAuditAnalyzer extends AbstractNpmAnalyzer {
7162 .addFilenames (YARN_PACKAGE_LOCK ).build ();
7263
7364 /**
74- * An expected error from `yarn audit --offline --verbose --json` that will
75- * be ignored.
65+ * The path to the `yarn` executable.
7666 */
77- private static final String EXPECTED_ERROR = "{\" type\" :\" error\" ,\" data\" :\" Can't make a request in "
78- + "offline mode (\\ \" https://registry.yarnpkg.com/-/npm/v1/security/audits\\ \" )\" }\n " ;
67+ private String yarnPath ;
7968
8069 /**
81- * The path to the `yarn` executable.
70+ * The version of the `yarn` executable.
8271 */
83- private String yarnPath ;
72+ private String yarnVersion ;
73+
74+
75+ /**
76+ * Extracts the major version from a version string.
77+ *
78+ * @return the major version (e.g., `4` from "4.2.1")
79+ */
80+ protected int getYarnMajorVersion () {
81+ if (StringUtils .isBlank (yarnVersion )) {
82+ throw new IllegalArgumentException ("Version string cannot be null or empty" );
83+ }
84+ try {
85+ var semver = new Semver (yarnVersion );
86+ return semver .getMajor ();
87+ } catch (SemverException e ) {
88+ throw new IllegalArgumentException ("Invalid version string format" , e );
89+ }
90+ }
8491
8592 /**
8693 * Analyzes the yarn lock file to determine vulnerable dependencies. Uses
8794 * yarn audit --offline to generate the payload to be sent to the NPM API.
8895 *
8996 * @param dependency the yarn lock file
90- * @param engine the analysis engine
97+ * @param engine the analysis engine
9198 * @throws AnalysisException thrown if there is an error analyzing the file
9299 */
93100 @ Override
@@ -110,6 +117,20 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An
110117 }
111118 }
112119
120+ /**
121+ * Analyzes the package.
122+ *
123+ * @param lockFile a reference to the package-lock.json
124+ * @param packageFile a reference to the package.json
125+ * @param dependency a reference to the dependency-object for the yarn.lock
126+ * @param dependencyMap a collection of module/version pairs; during
127+ * creation of the payload the dependency map is populated with the
128+ * module/version information.
129+ * @return a list of advisories
130+ * @throws AnalysisException thrown when there is an error creating or submitting the npm audit
131+ */
132+ protected abstract List <Advisory > analyzePackage (File lockFile , File packageFile , Dependency dependency , MultiValuedMap <String , String > dependencyMap ) throws AnalysisException ;
133+
113134 @ Override
114135 protected String getAnalyzerEnabledSettingKey () {
115136 return Settings .KEYS .ANALYZER_YARN_AUDIT_ENABLED ;
@@ -120,11 +141,6 @@ protected FileFilter getFileFilter() {
120141 return LOCK_FILE_FILTER ;
121142 }
122143
123- @ Override
124- public String getName () {
125- return "Yarn Audit Analyzer" ;
126- }
127-
128144 @ Override
129145 public AnalysisPhase getAnalysisPhase () {
130146 return AnalysisPhase .FINDING_ANALYSIS ;
@@ -145,7 +161,7 @@ protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationExcep
145161 }
146162 final List <String > args = new ArrayList <>();
147163 args .add (getYarn ());
148- args .add ("--help " );
164+ args .add ("--version " );
149165 final ProcessBuilder builder = new ProcessBuilder (args );
150166 LOGGER .debug ("Launching: {}" , args );
151167 try {
@@ -158,6 +174,11 @@ protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationExcep
158174 switch (exitValue ) {
159175 case expectedExitValue :
160176 LOGGER .debug ("{} is enabled." , getName ());
177+ yarnVersion = processReader .getOutput ();
178+ if (StringUtils .isBlank (yarnVersion )) {
179+ this .setEnabled (false );
180+ LOGGER .warn ("The {} has been disabled. Yarn version could not be determined." , getName ());
181+ }
161182 break ;
162183 case yarnExecutableNotFoundExitValue :
163184 default :
@@ -177,7 +198,7 @@ protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationExcep
177198 *
178199 * @return the path to `yarn`
179200 */
180- private String getYarn () {
201+ protected String getYarn () {
181202 final String value ;
182203 synchronized (this ) {
183204 if (yarnPath == null ) {
@@ -199,54 +220,24 @@ private String getYarn() {
199220 return value ;
200221 }
201222
202- private JsonObject fetchYarnAuditJson ( Dependency dependency , boolean skipDevDependencies ) throws AnalysisException {
203- final File folder = dependency . getActualFile (). getParentFile ();
204- if (! folder . isDirectory ()) {
205- throw new AnalysisException ( String . format ( "%s should have been a directory." , folder . getAbsolutePath ()));
206- }
223+ /**
224+ * Workaround 64k limitation of InputStream, redirect stdout to a file that we will read later
225+ * instead of reading directly stdout from Process's InputStream which is topped at 64k
226+ */
227+ protected String startAndReadStdoutToString ( ProcessBuilder builder ) throws AnalysisException {
207228 try {
208- final List <String > args = new ArrayList <>();
209-
210- args .add (getYarn ());
211- args .add ("audit" );
212- //offline audit is not supported - but the audit request is generated in the verbose output
213- args .add ("--offline" );
214- if (skipDevDependencies ) {
215- args .add ("--groups" );
216- args .add ("dependencies" );
217- }
218- args .add ("--json" );
219- args .add ("--verbose" );
220- final ProcessBuilder builder = new ProcessBuilder (args );
221- builder .directory (folder );
222- LOGGER .debug ("Launching: {}" , args );
223- // Workaround 64k limitation of InputStream, redirect stdout to a file that we will read later
224- // instead of reading directly stdout from Process's InputStream which is topped at 64k
225-
226229 final File tmpFile = getSettings ().getTempFile ("yarn_audit" , "json" );
227230 builder .redirectOutput (tmpFile );
228231 final Process process = builder .start ();
229232 try (ProcessReader processReader = new ProcessReader (process )) {
230233 processReader .readAll ();
231234 final String errOutput = processReader .getError ();
232235
233- if (!StringUtils .isBlank (errOutput ) && ! EXPECTED_ERROR . equals ( errOutput ) ) {
236+ if (!StringUtils .isBlank (errOutput )) {
234237 LOGGER .debug ("Process Error Out: {}" , errOutput );
235238 LOGGER .debug ("Process Out: {}" , processReader .getOutput ());
236239 }
237- final String verboseJson = new String (Files .readAllBytes (tmpFile .toPath ()), StandardCharsets .UTF_8 );
238- final String auditRequestJson = Arrays .stream (verboseJson .split ("\n " ))
239- .filter (line -> line .contains ("Audit Request" ))
240- .findFirst ().get ();
241- String auditRequest ;
242- try (JsonReader reader = Json .createReader (IOUtils .toInputStream (auditRequestJson , StandardCharsets .UTF_8 ))) {
243- final JsonObject jsonObject = reader .readObject ();
244- auditRequest = jsonObject .getString ("data" );
245- auditRequest = auditRequest .substring (15 );
246- }
247- LOGGER .debug ("Audit Request: {}" , auditRequest );
248-
249- return Json .createReader (IOUtils .toInputStream (auditRequest , StandardCharsets .UTF_8 )).readObject ();
240+ return new String (Files .readAllBytes (tmpFile .toPath ()), StandardCharsets .UTF_8 );
250241 } catch (InterruptedException ex ) {
251242 Thread .currentThread ().interrupt ();
252243 throw new AnalysisException ("Yarn audit process was interrupted." , ex );
@@ -255,55 +246,4 @@ private JsonObject fetchYarnAuditJson(Dependency dependency, boolean skipDevDepe
255246 throw new AnalysisException ("yarn audit failure; this error can be ignored if you are not analyzing projects with a yarn lockfile." , ioe );
256247 }
257248 }
258-
259- /**
260- * Analyzes the package and yarn lock files by extracting dependency
261- * information, creating a payload to submit to the npm audit API,
262- * submitting the payload, and returning the identified advisories.
263- *
264- * @param lockFile a reference to the package-lock.json
265- * @param packageFile a reference to the package.json
266- * @param dependency a reference to the dependency-object for the yarn.lock
267- * @param dependencyMap a collection of module/version pairs; during
268- * creation of the payload the dependency map is populated with the
269- * module/version information.
270- * @return a list of advisories
271- * @throws AnalysisException thrown when there is an error creating or
272- * submitting the npm audit API payload
273- */
274- private List <Advisory > analyzePackage (final File lockFile , final File packageFile ,
275- Dependency dependency , MultiValuedMap <String , String > dependencyMap )
276- throws AnalysisException {
277- try {
278- final boolean skipDevDependencies = getSettings ().getBoolean (Settings .KEYS .ANALYZER_NODE_AUDIT_SKIPDEV , false );
279- // Retrieves the contents of package-lock.json from the Dependency
280- final JsonObject lockJson = fetchYarnAuditJson (dependency , skipDevDependencies );
281- // Retrieves the contents of package-lock.json from the Dependency
282- final JsonObject packageJson ;
283- try (JsonReader packageReader = Json .createReader (Files .newInputStream (packageFile .toPath ()))) {
284- packageJson = packageReader .readObject ();
285- }
286- // Modify the payload to meet the NPM Audit API requirements
287- final JsonObject payload = NpmPayloadBuilder .build (lockJson , packageJson , dependencyMap , skipDevDependencies );
288-
289- // Submits the package payload to the nsp check service
290- return getSearcher ().submitPackage (payload );
291-
292- } catch (URLConnectionFailureException e ) {
293- this .setEnabled (false );
294- throw new AnalysisException ("Failed to connect to the NPM Audit API (YarnAuditAnalyzer); the analyzer "
295- + "is being disabled and may result in false negatives." , e );
296- } catch (IOException e ) {
297- LOGGER .debug ("Error reading dependency or connecting to NPM Audit API" , e );
298- this .setEnabled (false );
299- throw new AnalysisException ("Failed to read results from the NPM Audit API (YarnAuditAnalyzer); "
300- + "the analyzer is being disabled and may result in false negatives." , e );
301- } catch (JsonException e ) {
302- throw new AnalysisException (String .format ("Failed to parse %s file from the NPM Audit API "
303- + "(YarnAuditAnalyzer)." , lockFile .getPath ()), e );
304- } catch (SearchException ex ) {
305- LOGGER .error ("YarnAuditAnalyzer failed on {}" , dependency .getActualFilePath ());
306- throw ex ;
307- }
308- }
309249}
0 commit comments