3030public class SonarQubeJsonReader extends Reader {
3131
3232 public TestResults parse ( File f ) throws Exception {
33+
34+ TestResults tr = new TestResults ( "SonarQube" , false , TestResults .ToolType .SAST );
35+
36+ // If the filename includes an elapsed time in seconds (e.g., TOOLNAME-seconds.xml),
37+ // set the compute time on the score card.
38+ tr .setTime (f );
39+
3340 String content = new String (Files .readAllBytes (Paths .get (f .getPath ())));
3441
3542 JSONObject obj = new JSONObject (content );
3643 // int version = obj.getInt( "formatVersion" );
3744 JSONArray arr ;
45+
46+ boolean hotSpotIssue = true ;
47+
48+ // Figure out if there are quality issues or security hotspots in the JSON file
49+ // Each has a different JSON format.
3850 try {
3951 arr = obj .getJSONArray ("issues" );
52+ hotSpotIssue = false ;
4053 } catch (JSONException e ) {
41- System .out .println ("ERROR: Couldn't find 'issues' element in SonarQube JSON results."
42- + " Maybe not SonarQube results file?" );
43- return null ;
54+ try {
55+ arr = obj .getJSONArray ("hotspots" );
56+ } catch (JSONException e2 ) {
57+ System .out .println ("ERROR: Couldn't find 'issues' or 'hotspots' element in SonarQube JSON results."
58+ + " Maybe not SonarQube results file?" );
59+ return null ;
60+ }
4461 }
4562
46- TestResults tr = new TestResults ( "SonarJava" , false , TestResults .ToolType .SAST );
47-
48- // If the filename includes an elapsed time in seconds (e.g., TOOLNAME-seconds.xml),
49- // set the compute time on the score card.
50- tr .setTime (f );
51-
5263 int numIssues = arr .length ();
53- for (int i = 0 ; i < numIssues ; i ++)
54- {
55- TestCaseResult tcr = parseSonarQubeFinding ( arr .getJSONObject (i ) );
64+ for (int i = 0 ; i < numIssues ; i ++) {
65+
66+ TestCaseResult tcr = (hotSpotIssue ? parseSonarQubeHotSpotIssue ( arr .getJSONObject (i ) ) :
67+ parseSonarQubeQualityIssue ( arr .getJSONObject (i ) ));
5668 if ( tcr != null ) {
5769 tr .put ( tcr );
5870 }
@@ -61,7 +73,7 @@ public TestResults parse( File f ) throws Exception {
6173 return tr ;
6274 }
6375
64- /**
76+ /** -- Example of Quality Issue JSON object
6577 VULNERABILITY",
6678 "tags":["cwe","owasp-a2","owasp-a6"],
6779 "component":"org.owasp:benchmark:src\/main\/java\/org\/owasp\/benchmark\/testcode\/BenchmarkTest02710.java",
@@ -86,7 +98,10 @@ public TestResults parse( File f ) throws Exception {
8698
8799 **/
88100
89- private TestCaseResult parseSonarQubeFinding (JSONObject finding ) {
101+ // Quality Issues are normal SonarQube findings that are mostly not relevant to security
102+ // However, there are a small number of security issues that do show up this way so we have
103+ // to support both
104+ private TestCaseResult parseSonarQubeQualityIssue (JSONObject finding ) {
90105 try {
91106 TestCaseResult tcr = new TestCaseResult ();
92107 String filename = null ;
@@ -116,5 +131,103 @@ private TestCaseResult parseSonarQubeFinding(JSONObject finding ) {
116131 return null ;
117132 }
118133
134+ // The parseSonarQubeQualityIssue() method above relies on the SQUID # mapping method in SonarQubeReader.cweLookup()
135+
136+ /** -- Example of HotSpot Issue JSON object
137+ "hotspots": [
138+ {
139+ "key": "AXYEidyZsoEy1bftafT5",
140+ "component": "owasp-benchmark-sonarce:src/main/java/org/owasp/benchmark/testcode/BenchmarkTest00008.java",
141+ "project": "owasp-benchmark-sonarce",
142+ "securityCategory": "sql-injection",
143+ "vulnerabilityProbability": "HIGH",
144+ "status": "TO_REVIEW",
145+ "line": 58,
146+ "message": "Ensure that string concatenation is required and safe for this SQL query.",
147+ 148+ "creationDate": "2015-08-26T05:13:42+0200",
149+ "updateDate": "2020-11-26T12:53:38+0100"
150+ },
151+ **/
152+
153+ // Hotspot Issues are SonarQube security findings.
154+ private TestCaseResult parseSonarQubeHotSpotIssue (JSONObject finding ) {
155+ try {
156+ TestCaseResult tcr = new TestCaseResult ();
157+ String filename = null ;
158+
159+ filename = finding .getString ("component" );
160+ filename = filename .replaceAll ( "\\ \\ " , "/" ); // In case there are \ instead of / in the path
161+ filename = filename .substring ( filename .lastIndexOf ( '/' ) );
162+ if ( filename .contains ( BenchmarkScore .TESTCASENAME ) ) {
163+ String testNumber = filename .substring ( BenchmarkScore .TESTCASENAME .length () + 1 ,
164+ filename .length () - 5 );
165+ tcr .setNumber ( Integer .parseInt ( testNumber ) );
166+ String secCat = finding .getString ("securityCategory" );
167+ if ( secCat == null || secCat .equals ("none" ) ) {
168+ return null ;
169+ }
170+ int cwe = securityCategoryCWELookup (secCat , finding .getString ("message" ));
171+ tcr .setCWE ( cwe );
172+ tcr .setCategory ( secCat );
173+ tcr .setEvidence ( "vulnerabilityProbability: " + finding .getString ("vulnerabilityProbability" ) );
174+ }
175+
176+ return tcr ;
177+ } catch (Exception e ) {
178+ e .printStackTrace ();
179+ }
180+ return null ;
181+ }
182+
183+ /*
184+ * Some of these findings are badly mapped. For example:
185+ * "securityCategory": "xss",
186+ * "message": "Make sure creating this cookie without the \"HttpOnly\" flag is safe.",
187+ * While HttpOnly is a feature to help defend against XSS, it should really be mapped to
188+ * CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag. So we use the 'message' description
189+ * in some findings to move such issues to the 'right' CWE.
190+ * As such, we specifically look at the message in some cases to fix the mapping.
191+ */
192+ public static int securityCategoryCWELookup (String secCat , String message ) {
193+ // Not sure where to look up all the possible security categories in SonarQube, but the mappings
194+ // seem obvious enough.
195+
196+ // Given their horrible mapping scheme, we check each message to detect whether their might be a new
197+ // 'message' mapped to an existing CWE (that might be wrong).
198+ if ( !("Make sure that using this pseudorandom number generator is safe here." .equals (message ) ||
199+ "Ensure that string concatenation is required and safe for this SQL query." .equals (message ) ||
200+ "Make sure creating this cookie without the \" secure\" flag is safe here." .equals (message ) ||
201+ "Make sure that hashing data is safe here." .equals (message ) ||
202+ "Make sure creating this cookie without the \" HttpOnly\" flag is safe." .equals (message )) )
203+ {
204+ System .out .println ("WARN: Found new SonarQube HotSpot rule not seen before. Category: "
205+ + secCat + " with message: '" + message + "'" );
206+ }
207+
208+ switch ( secCat ) {
209+
210+ case "sql-injection" : return 89 ; // "Ensure that string concatenation is required and safe for this SQL query."
211+ case "insecure-conf" : return 614 ; // "Make sure creating this cookie without the \"secure\" flag is safe here."
212+ case "xss" : // "Make sure creating this cookie without the \"HttpOnly\" flag is safe."
213+ {
214+ if (message != null && message .contains ("HttpOnly" )) return 1004 ;
215+ else return 79 ; // Actual XSS CWE
216+ }
217+ case "weak-cryptography" : // "Make sure that using this pseudorandom number generator is safe here."
218+ { // or "Make sure that hashing data is safe here."
219+ if (message != null ) {
220+ if (message .contains ("pseudorandom" )) return 330 ;
221+ if (message .contains ("hashing" )) return 328 ;
222+ else return 0000 ;
223+ }
224+ else return 327 ; // Actual Weak Crypto CWE
225+ }
226+ default : System .out .println ( "WARN: Failed to translate SonarQube security category: " + secCat );
227+ }
228+
229+ return -1 ;
230+ }
231+
119232 // This parser relies on the SQUID # mapping method in SonarQubeReader.cweLookup()
120233}
0 commit comments