]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10978 new WS to return security reports data
authorJulien HENRY <julien.henry@sonarsource.com>
Mon, 9 Jul 2018 21:57:32 +0000 (23:57 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 17 Jul 2018 18:21:24 +0000 (20:21 +0200)
15 files changed:
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndex.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWs.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWsModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/ShowAction.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/securityreport/ws/ShowActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/empty.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/owaspNoCwe.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/owaspWithCwe.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/sansWithCwe.json [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-security.proto [new file with mode: 0644]

index 27303d4b5085bae7c2b63f3d1e33c8304b5decfb..a9794254c7733fb3a9aad91ee91ada9bbaa1bcb0 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.issue.index;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
@@ -30,6 +31,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.OptionalInt;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
@@ -46,6 +48,7 @@ import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.indices.TermsLookup;
 import org.elasticsearch.search.aggregations.AggregationBuilder;
 import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.HasAggregations;
 import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
 import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter;
 import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
@@ -63,6 +66,8 @@ import org.elasticsearch.search.aggregations.metrics.valuecount.InternalValueCou
 import org.elasticsearch.search.sort.FieldSortBuilder;
 import org.joda.time.DateTimeZone;
 import org.joda.time.Duration;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rule.Severity;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.System2;
@@ -95,6 +100,10 @@ import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
 import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
 import static org.sonar.server.es.BaseDoc.epochMillisToEpochSeconds;
 import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars;
+import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_INSECURE_INTERACTION;
+import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_POROUS_DEFENSES;
+import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_RISKY_RESOURCE;
+import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD;
 import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID;
 import static org.sonar.server.issue.index.IssueIndexDefinition.INDEX_TYPE_ISSUE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_FACET_MODE_DEBT;
@@ -789,4 +798,114 @@ public class IssueIndex {
       .collect(MoreCollectors.toList(branchUuids.size()));
   }
 
+  public List<SecurityStandardCategoryStatistics> getSansTop25Report(String projectUuid, boolean includeCwe) {
+    SearchRequestBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid);
+    Stream.of(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES).forEach(sansCategory -> {
+      AggregationBuilder sansCategoryAggs = AggregationBuilders
+        .filter(sansCategory, boolQuery()
+          .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, sansCategory)));
+      request.addAggregation(addSecurityReportSubAggregations(sansCategoryAggs, includeCwe));
+    });
+    return processSecurityReportSearchResults(request, includeCwe);
+  }
+
+  public List<SecurityStandardCategoryStatistics> getOwaspTop10Report(String projectUuid, boolean includeCwe) {
+    SearchRequestBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid);
+    Stream.concat(IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i), Stream.of(UNKNOWN_STANDARD)).forEach(owaspCategory -> {
+      AggregationBuilder owaspCategoryAggs = AggregationBuilders
+        .filter(owaspCategory, boolQuery()
+          .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, owaspCategory)));
+      request.addAggregation(addSecurityReportSubAggregations(owaspCategoryAggs, includeCwe));
+    });
+    return processSecurityReportSearchResults(request, includeCwe);
+  }
+
+  private static List<SecurityStandardCategoryStatistics> processSecurityReportSearchResults(SearchRequestBuilder request, boolean includeCwe) {
+    SearchResponse response = request.get();
+    return response.getAggregations().asList().stream()
+      .map(c -> processSecurityReportIssueSearchResults((InternalFilter) c, includeCwe))
+      .collect(MoreCollectors.toList());
+  }
+
+  private static SecurityStandardCategoryStatistics processSecurityReportIssueSearchResults(InternalFilter categoryBucket, boolean includeCwe) {
+    List<SecurityStandardCategoryStatistics> children = new ArrayList<>();
+
+    if (includeCwe) {
+      ((StringTerms) categoryBucket.getAggregations().get("cwe")).getBuckets()
+        .forEach(cweBucket -> children.add(processSecurityReportCategorySearchResults(cweBucket, cweBucket.getKeyAsString(), null)));
+    }
+
+    return processSecurityReportCategorySearchResults(categoryBucket, categoryBucket.getName(), children);
+  }
+
+  private static SecurityStandardCategoryStatistics processSecurityReportCategorySearchResults(HasAggregations categoryBucket, String categoryName,
+    @Nullable List<SecurityStandardCategoryStatistics> children) {
+    List<StringTerms.Bucket> severityBuckets = ((StringTerms) ((InternalFilter) categoryBucket.getAggregations().get("vulnerabilities")).getAggregations().get("severity"))
+      .getBuckets();
+    long vulnerabilities = severityBuckets.stream().mapToLong(b -> ((InternalValueCount) b.getAggregations().get("count")).getValue()).sum();
+    // Worst severity having at least one issue
+    OptionalInt severityRating = severityBuckets.stream()
+      .filter(b -> ((InternalValueCount) b.getAggregations().get("count")).getValue() != 0)
+      .mapToInt(b -> Severity.ALL.indexOf(b.getKeyAsString()) + 1)
+      .max();
+
+    long openSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("openSecurityHotspots")).getAggregations().get("count"))
+      .getValue();
+    long toReviewSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("toReviewSecurityHotspots")).getAggregations().get("count"))
+      .getValue();
+    long wontFixSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("wontFixSecurityHotspots")).getAggregations().get("count"))
+      .getValue();
+
+    return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots, openSecurityHotspots,
+      wontFixSecurityHotspots, children);
+  }
+
+  private static AggregationBuilder addSecurityReportSubAggregations(AggregationBuilder categoriesAggs, boolean includeCwe) {
+    AggregationBuilder aggregationBuilder = addSecurityReportIssueCountAggregations(categoriesAggs);
+    if (includeCwe) {
+      categoriesAggs
+        .subAggregation(addSecurityReportIssueCountAggregations(AggregationBuilders.terms("cwe").field(IssueIndexDefinition.FIELD_ISSUE_CWE)));
+    }
+    return aggregationBuilder;
+  }
+
+  private static AggregationBuilder addSecurityReportIssueCountAggregations(AggregationBuilder categoryAggs) {
+    return categoryAggs
+      .subAggregation(
+        AggregationBuilders.filter("vulnerabilities", boolQuery()
+          .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.VULNERABILITY.name()))
+          .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION)))
+          .subAggregation(
+            AggregationBuilders.terms("severity").field(IssueIndexDefinition.FIELD_ISSUE_SEVERITY)
+              .subAggregation(
+                AggregationBuilders.count("count").field(IssueIndexDefinition.FIELD_ISSUE_KEY))))
+      .subAggregation(AggregationBuilders.filter("openSecurityHotspots", boolQuery()
+        .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name()))
+        .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION)))
+        .subAggregation(
+          AggregationBuilders.count("count").field(IssueIndexDefinition.FIELD_ISSUE_KEY)))
+      .subAggregation(AggregationBuilders.filter("toReviewSecurityHotspots", boolQuery()
+        .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name()))
+        .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_RESOLVED))
+        .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_FIXED)))
+        .subAggregation(
+          AggregationBuilders.count("count").field(IssueIndexDefinition.FIELD_ISSUE_KEY)))
+      .subAggregation(AggregationBuilders.filter("wontFixSecurityHotspots", boolQuery()
+        .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name()))
+        .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_RESOLVED))
+        .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_WONT_FIX)))
+        .subAggregation(
+          AggregationBuilders.count("count").field(IssueIndexDefinition.FIELD_ISSUE_KEY)));
+  }
+
+  private SearchRequestBuilder prepareNonClosedVulnerabilitiesAndHotspotSearch(String projectUuid) {
+    return client.prepareSearch(IssueIndexDefinition.INDEX_TYPE_ISSUE)
+      .setQuery(
+        boolQuery()
+          .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, projectUuid))
+          .filter(termsQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name(), RuleType.VULNERABILITY.name()))
+          .mustNot(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_CLOSED)))
+      .setSize(0);
+  }
+
 }
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java
new file mode 100644 (file)
index 0000000..291c394
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.issue.index;
+
+import java.util.List;
+import java.util.OptionalInt;
+import javax.annotation.Nullable;
+
+public class SecurityStandardCategoryStatistics {
+
+  private final String category;
+  private final long vulnerabilities;
+  private final OptionalInt vulnerabiliyRating;
+  private final long toReviewSecurityHotspots;
+  private final long openSecurityHotspots;
+  private final long wontFixSecurityHotspots;
+  private final List<SecurityStandardCategoryStatistics> children;
+
+  public SecurityStandardCategoryStatistics(String category, long vulnerabilities, OptionalInt vulnerabiliyRating, long toReviewSecurityHotspots, long openSecurityHotspots,
+    long wontFixSecurityHotspots, @Nullable List<SecurityStandardCategoryStatistics> children) {
+    this.category = category;
+    this.vulnerabilities = vulnerabilities;
+    this.vulnerabiliyRating = vulnerabiliyRating;
+    this.toReviewSecurityHotspots = toReviewSecurityHotspots;
+    this.openSecurityHotspots = openSecurityHotspots;
+    this.wontFixSecurityHotspots = wontFixSecurityHotspots;
+    this.children = children;
+  }
+
+  public String getCategory() {
+    return category;
+  }
+
+  public long getVulnerabilities() {
+    return vulnerabilities;
+  }
+
+  public OptionalInt getVulnerabiliyRating() {
+    return vulnerabiliyRating;
+  }
+
+  public long getToReviewSecurityHotspots() {
+    return toReviewSecurityHotspots;
+  }
+
+  public long getOpenSecurityHotspots() {
+    return openSecurityHotspots;
+  }
+
+  public long getWontFixSecurityHotspots() {
+    return wontFixSecurityHotspots;
+  }
+
+  public List<SecurityStandardCategoryStatistics> getChildren() {
+    return children;
+  }
+}
index 1c3e5ca93aac9a79f9d0ba1ad421784378f56b6f..ea07a6587bf4de3b5dc04d8e5ac355c5952e20ff 100644 (file)
@@ -27,6 +27,7 @@ import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.OptionalInt;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.assertj.core.api.Fail;
@@ -38,6 +39,7 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.System2;
 import org.sonar.api.utils.internal.TestSystem2;
 import org.sonar.db.DbTester;
@@ -83,6 +85,10 @@ import static org.sonar.db.rule.RuleTesting.newRule;
 import static org.sonar.db.user.GroupTesting.newGroupDto;
 import static org.sonar.db.user.UserTesting.newUserDto;
 import static org.sonar.server.issue.IssueDocTesting.newDoc;
+import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_INSECURE_INTERACTION;
+import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_POROUS_DEFENSES;
+import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_RISKY_RESOURCE;
+import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD;
 
 public class IssueIndexTest {
 
@@ -1444,6 +1450,197 @@ public class IssueIndexTest {
     assertThat(underTest.searchBranchStatistics(project.uuid(), singletonList("unknown"))).isEmpty();
   }
 
+  @Test
+  public void test_getOwaspTop10Report_dont_count_vulnerabilities_from_other_projects() {
+    OrganizationDto org = newOrganizationDto();
+    ComponentDto project = newPrivateProjectDto(org);
+    ComponentDto another = newPrivateProjectDto(org);
+    indexIssues(
+      newDoc("anotherProject", another).setOwaspTop10(singletonList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL),
+      newDoc("openvul1", project).setOwaspTop10(singletonList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR));
+
+    List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false);
+    assertThat(owaspTop10Report)
+      .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+        SecurityStandardCategoryStatistics::getVulnerabiliyRating)
+      .contains(
+        tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */));
+
+  }
+
+  @Test
+  public void test_getOwaspTop10Report_dont_count_closed_vulnerabilities() {
+    OrganizationDto org = newOrganizationDto();
+    ComponentDto project = newPrivateProjectDto(org);
+    indexIssues(
+      newDoc("openvul1", project).setOwaspTop10(asList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR),
+      newDoc("notopenvul", project).setOwaspTop10(asList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_CLOSED).setResolution(Issue.RESOLUTION_FIXED)
+        .setSeverity(Severity.BLOCKER));
+
+    List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false);
+    assertThat(owaspTop10Report)
+      .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+        SecurityStandardCategoryStatistics::getVulnerabiliyRating)
+      .contains(
+        tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */));
+  }
+
+  @Test
+  public void test_getOwaspTop10Report_dont_count_old_vulnerabilities() {
+    OrganizationDto org = newOrganizationDto();
+    ComponentDto project = newPrivateProjectDto(org);
+    indexIssues(
+      // Previous vulnerabilities in projects that are not reanalyzed will have no owasp nor cwe attributes (not even 'unknown')
+      newDoc("openvulNotReindexed", project).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR));
+
+    List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false);
+    assertThat(owaspTop10Report)
+      .extracting(SecurityStandardCategoryStatistics::getVulnerabilities,
+        SecurityStandardCategoryStatistics::getVulnerabiliyRating)
+      .containsOnly(
+        tuple(0L, OptionalInt.empty()));
+  }
+
+  @Test
+  public void test_getOwaspTop10Report_dont_count_hotspots_from_other_projects() {
+    OrganizationDto org = newOrganizationDto();
+    ComponentDto project = newPrivateProjectDto(org);
+    ComponentDto another = newPrivateProjectDto(org);
+    indexIssues(
+      newDoc("openhotspot1", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN),
+      newDoc("anotherProject", another).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN));
+
+    List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false);
+    assertThat(owaspTop10Report)
+      .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getOpenSecurityHotspots)
+      .contains(
+        tuple("a1", 1L /* openhotspot1 */));
+  }
+
+  @Test
+  public void test_getOwaspTop10Report_dont_count_closed_hotspots() {
+    OrganizationDto org = newOrganizationDto();
+    ComponentDto project = newPrivateProjectDto(org);
+    indexIssues(
+      newDoc("openhotspot1", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN),
+      newDoc("closedHotspot", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_CLOSED)
+        .setResolution(Issue.RESOLUTION_FIXED));
+
+    List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false);
+    assertThat(owaspTop10Report)
+      .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getOpenSecurityHotspots)
+      .contains(
+        tuple("a1", 1L /* openhotspot1 */));
+  }
+
+  @Test
+  public void test_getOwaspTop10Report_aggregation_no_cwe() {
+    List<SecurityStandardCategoryStatistics> owaspTop10Report = indexIssuesAndAssertOwaspReport(false);
+
+    assertThat(owaspTop10Report).allMatch(category -> category.getChildren().isEmpty());
+  }
+
+  @Test
+  public void test_getOwaspTop10Report_aggregation_with_cwe() {
+    List<SecurityStandardCategoryStatistics> owaspTop10Report = indexIssuesAndAssertOwaspReport(true);
+
+    Map<String, List<SecurityStandardCategoryStatistics>> cweByOwasp = owaspTop10Report.stream()
+      .collect(Collectors.toMap(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getChildren));
+
+    assertThat(cweByOwasp.get("a1")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+      SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots,
+      SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots)
+      .containsExactlyInAnyOrder(
+        tuple("123", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L),
+        tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L),
+        tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 0L));
+    assertThat(cweByOwasp.get("a3")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+      SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots,
+      SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots)
+      .containsExactlyInAnyOrder(
+        tuple("123", 2L /* openvul1, openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L),
+        tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 1L /* toReviewHotspot */, 0L),
+        tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 0L));
+  }
+
+  private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspReport(boolean includeCwe) {
+    OrganizationDto org = newOrganizationDto();
+    ComponentDto project = newPrivateProjectDto(org);
+    ComponentDto another = newPrivateProjectDto(org);
+    indexIssues(
+      newDoc("openvul1", project).setOwaspTop10(asList("a1", "a3")).setCwe(asList("123", "456")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN)
+        .setSeverity(Severity.MAJOR),
+      newDoc("openvul2", project).setOwaspTop10(asList("a3", "a6")).setCwe(asList("123")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED)
+        .setSeverity(Severity.MINOR),
+      newDoc("notowaspvul", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN)
+        .setSeverity(Severity.CRITICAL),
+      newDoc("openhotspot1", project).setOwaspTop10(asList("a1", "a3")).setCwe(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT)
+        .setStatus(Issue.STATUS_OPEN),
+      newDoc("openhotspot2", project).setOwaspTop10(asList("a3", "a6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REOPENED),
+      newDoc("toReviewHotspot", project).setOwaspTop10(asList("a5", "a3")).setCwe(asList("456")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED)
+        .setResolution(Issue.RESOLUTION_FIXED),
+      newDoc("WFHotspot", project).setOwaspTop10(asList("a3", "a8")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED)
+        .setResolution(Issue.RESOLUTION_WONT_FIX),
+      newDoc("notowasphotspot", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN));
+
+    List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), includeCwe);
+    assertThat(owaspTop10Report)
+      .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+        SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots,
+        SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots)
+      .containsExactlyInAnyOrder(
+        tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* openhotspot1 */, 0L, 0L),
+        tuple("a2", 0L, OptionalInt.empty(), 0L, 0L, 0L),
+        tuple("a3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* openhotspot1,openhotspot2 */, 1L /* toReviewHotspot */, 1L /* WFHotspot */),
+        tuple("a4", 0L, OptionalInt.empty(), 0L, 0L, 0L),
+        tuple("a5", 0L, OptionalInt.empty(), 0L, 1L/* toReviewHotspot */, 0L),
+        tuple("a6", 1L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* openhotspot2 */, 0L, 0L),
+        tuple("a7", 0L, OptionalInt.empty(), 0L, 0L, 0L),
+        tuple("a8", 0L, OptionalInt.empty(), 0L, 0L, 1L /* WFHotspot */),
+        tuple("a9", 0L, OptionalInt.empty(), 0L, 0L, 0L),
+        tuple("a10", 0L, OptionalInt.empty(), 0L, 0L, 0L),
+        tuple("unknown", 1L /* notowaspvul */, OptionalInt.of(4) /* CRITICAL = D */, 1L /* notowasphotspot */, 0L, 0L));
+    return owaspTop10Report;
+  }
+
+  @Test
+  public void test_getSansTop25Report_aggregation() {
+    OrganizationDto org = newOrganizationDto();
+    ComponentDto project = newPrivateProjectDto(org);
+    indexIssues(
+      newDoc("openvul1", project).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN)
+        .setSeverity(Severity.MAJOR),
+      newDoc("openvul2", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED)
+        .setSeverity(Severity.MINOR),
+      newDoc("notopenvul", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_CLOSED)
+        .setResolution(Issue.RESOLUTION_FIXED)
+        .setSeverity(Severity.BLOCKER),
+      newDoc("notsansvul", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN)
+        .setSeverity(Severity.CRITICAL),
+      newDoc("openhotspot1", project).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT)
+        .setStatus(Issue.STATUS_OPEN),
+      newDoc("openhotspot2", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT)
+        .setStatus(Issue.STATUS_REOPENED),
+      newDoc("toReviewHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED)
+        .setResolution(Issue.RESOLUTION_FIXED),
+      newDoc("WFHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED)
+        .setResolution(Issue.RESOLUTION_WONT_FIX),
+      newDoc("notowasphotspot", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN));
+
+    List<SecurityStandardCategoryStatistics> sansTop25Report = underTest.getSansTop25Report(project.uuid(), false);
+    assertThat(sansTop25Report)
+      .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+        SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots,
+        SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots)
+      .containsExactlyInAnyOrder(
+        tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* openhotspot1 */, 0L, 0L),
+        tuple(SANS_TOP_25_RISKY_RESOURCE, 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* openhotspot1,openhotspot2 */, 1L /* toReviewHotspot */,
+          1L /* WFHotspot */),
+        tuple(SANS_TOP_25_POROUS_DEFENSES, 1L /* openvul2 */, OptionalInt.of(2)/* MINOR = B */, 1L/* openhotspot2 */, 0L, 0L));
+
+    assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty());
+  }
+
   private void addIssues(ComponentDto component, int bugs, int vulnerabilities, int codeSmelles) {
     List<IssueDoc> issues = new ArrayList<>();
     IntStream.range(0, bugs).forEach(b -> issues.add(newDoc(component).setType(BUG).setResolution(null)));
index 0d9fc391d8b6304c43597c4e4aabb594b8b662a6..6ecf880a1dc5147b92abbcfc9a12de4befa34716 100644 (file)
@@ -144,7 +144,7 @@ public class IssueIndexerTest {
 
   @Test
   public void verify_security_standards_indexation() {
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setSecurityStandards(new HashSet<>(Arrays.asList("cwe:123,owaspTop10:a3,cwe:863"))));
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setSecurityStandards(new HashSet<>(Arrays.asList("cwe:123", "owaspTop10:a3", "cwe:863"))));
     ComponentDto project = db.components().insertPrivateProject(organization);
     ComponentDto dir = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java/foo"));
     ComponentDto file = db.components().insertComponent(newFileDto(project, dir, "F1"));
index e836e9aec429f5f34d669e85fe992a773ac34964..9d6eee10437578c6f96d01d44be2bf377150d923 100644 (file)
@@ -187,6 +187,7 @@ import org.sonar.server.rule.ws.RuleQueryFactory;
 import org.sonar.server.rule.ws.RuleWsSupport;
 import org.sonar.server.rule.ws.RulesWs;
 import org.sonar.server.rule.ws.TagsAction;
+import org.sonar.server.securityreport.ws.SecurityReportsWsModule;
 import org.sonar.server.setting.ws.SettingsWsModule;
 import org.sonar.server.source.HtmlSourceDecorator;
 import org.sonar.server.source.SourceService;
@@ -434,6 +435,9 @@ public class PlatformLevel4 extends PlatformLevel {
       DoNotFixNotificationDispatcher.newMetadata(),
       NewIssuesNotificationFactory.class,
 
+      // Security reports
+      SecurityReportsWsModule.class,
+
       // issues actions
       AssignAction.class,
       SetTypeAction.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWs.java b/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWs.java
new file mode 100644 (file)
index 0000000..34fe10a
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.securityreport.ws;
+
+import org.sonar.api.server.ws.WebService;
+
+public class SecurityReportsWs implements WebService {
+
+  private final SecurityReportsWsAction[] actions;
+
+  public SecurityReportsWs(SecurityReportsWsAction... actions) {
+    this.actions = actions;
+  }
+
+  @Override
+  public void define(Context context) {
+    NewController controller = context.createController("api/security_reports");
+    controller.setDescription("Return data needed by the security reports");
+    controller.setSince("7.3");
+    for (SecurityReportsWsAction action : actions) {
+      action.define(controller);
+    }
+    controller.done();
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWsAction.java
new file mode 100644 (file)
index 0000000..2af1c9e
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.securityreport.ws;
+
+import org.sonar.server.ws.WsAction;
+
+interface SecurityReportsWsAction extends WsAction {
+  // Marker interface
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/SecurityReportsWsModule.java
new file mode 100644 (file)
index 0000000..6555a1c
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.securityreport.ws;
+
+import org.sonar.core.platform.Module;
+
+public class SecurityReportsWsModule extends Module {
+  @Override
+  protected void configureModule() {
+    add(
+      SecurityReportsWs.class,
+      ShowAction.class);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/ShowAction.java b/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/ShowAction.java
new file mode 100644 (file)
index 0000000..5f5ea13
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.securityreport.ws;
+
+import java.util.List;
+import java.util.function.Function;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.SecurityStandardCategoryStatistics;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.SecurityReports;
+
+import static java.lang.Integer.parseInt;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SANS_TOP_25;
+
+public class ShowAction implements SecurityReportsWsAction {
+
+  private static final String PARAM_PROJECT = "project";
+  private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_INCLUDE_DISTRIBUTION = "includeDistribution";
+  private static final String PARAM_STANDARD = "standard";
+  private static final String OWASP_CAT_PREFIX = "a";
+
+  private final UserSession userSession;
+  private final ComponentFinder componentFinder;
+  private final IssueIndex issueIndex;
+  private final DbClient dbClient;
+
+  public ShowAction(UserSession userSession, ComponentFinder componentFinder, IssueIndex issueIndex, DbClient dbClient) {
+    this.userSession = userSession;
+    this.componentFinder = componentFinder;
+    this.issueIndex = issueIndex;
+    this.dbClient = dbClient;
+  }
+
+  @Override
+  public void define(WebService.NewController controller) {
+    WebService.NewAction action = controller
+      .createAction("show")
+      .setHandler(this)
+      .setDescription("Return data used by security reports")
+      .setSince("7.3")
+      .setInternal(true);
+
+    action.createParam(PARAM_PROJECT)
+      .setDescription("Project key")
+      .setRequired(true);
+    action.createParam(PARAM_BRANCH)
+      .setDescription("Branch name")
+      .setExampleValue("branch-2.0");
+    action.createParam(PARAM_STANDARD)
+      .setDescription("Security standard")
+      .setPossibleValues(PARAM_OWASP_TOP_10, PARAM_SANS_TOP_25)
+      .setRequired(true);
+    action.createParam(PARAM_INCLUDE_DISTRIBUTION)
+      .setDescription("To return CWE distribution")
+      .setBooleanPossibleValues()
+      .setDefaultValue("false");
+  }
+
+  @Override
+  public final void handle(Request request, Response response) {
+    String projectKey = request.mandatoryParam(PARAM_PROJECT);
+    ComponentDto projectDto;
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      projectDto = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, projectKey, request.param(PARAM_BRANCH), null);
+    }
+    userSession.checkComponentPermission(USER, projectDto);
+    String standard = request.mandatoryParam(PARAM_STANDARD);
+    boolean includeCwe = request.mandatoryParamAsBoolean(PARAM_INCLUDE_DISTRIBUTION);
+    switch (standard) {
+      case PARAM_OWASP_TOP_10:
+        List<SecurityStandardCategoryStatistics> owaspCategories = issueIndex.getOwaspTop10Report(projectDto.uuid(), includeCwe)
+          .stream()
+          .sorted(comparing(ShowAction::index))
+          .collect(toList());
+        writeResponse(request, response, owaspCategories);
+        break;
+      case PARAM_SANS_TOP_25:
+        writeResponse(request, response, issueIndex.getSansTop25Report(projectDto.uuid(), includeCwe));
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported standard: '" + standard + "'");
+    }
+  }
+
+  private static Integer index(SecurityStandardCategoryStatistics owaspCat) {
+    if (owaspCat.getCategory().startsWith(OWASP_CAT_PREFIX)) {
+      return parseInt(owaspCat.getCategory().substring(OWASP_CAT_PREFIX.length()));
+    }
+    // unknown
+    return 11;
+  }
+
+  private static void writeResponse(Request request, Response response, List<SecurityStandardCategoryStatistics> categories) {
+    SecurityReports.ShowWsResponse.Builder builder = SecurityReports.ShowWsResponse.newBuilder();
+    categories.forEach(cat -> {
+      SecurityReports.SecurityStandardCategoryStatistics.Builder catBuilder = SecurityReports.SecurityStandardCategoryStatistics.newBuilder();
+      catBuilder
+        .setCategory(cat.getCategory())
+        .setVulnerabilities(cat.getVulnerabilities());
+      cat.getVulnerabiliyRating().ifPresent(catBuilder::setVulnerabilityRating);
+      catBuilder
+        .setOpenSecurityHotspots(cat.getOpenSecurityHotspots())
+        .setToReviewSecurityHotspots(cat.getToReviewSecurityHotspots())
+        .setWontFixSecurityHotspots(cat.getWontFixSecurityHotspots());
+      if (cat.getChildren() != null) {
+        cat.getChildren().stream()
+          .sorted(comparing(cweIndex()))
+          .forEach(cwe -> {
+            SecurityReports.CweStatistics.Builder cweBuilder = SecurityReports.CweStatistics.newBuilder();
+            cweBuilder
+              .setCwe(cwe.getCategory())
+              .setVulnerabilities(cwe.getVulnerabilities());
+            cwe.getVulnerabiliyRating().ifPresent(cweBuilder::setVulnerabilityRating);
+            cweBuilder
+              .setOpenSecurityHotspots(cwe.getOpenSecurityHotspots())
+              .setToReviewSecurityHotspots(cwe.getToReviewSecurityHotspots())
+              .setWontFixSecurityHotspots(cwe.getWontFixSecurityHotspots());
+            catBuilder.addDistribution(cweBuilder);
+          });
+      }
+      builder.addCategories(catBuilder);
+    });
+
+    writeProtobuf(builder.build(), request, response);
+  }
+
+  private static Function<SecurityStandardCategoryStatistics, Integer> cweIndex() {
+    return securityStandardCategoryStatistics -> {
+      String category = securityStandardCategoryStatistics.getCategory();
+      return category.equals(UNKNOWN_STANDARD) ? Integer.MAX_VALUE : parseInt(category);
+    };
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/securityreport/ws/ShowActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/securityreport/ws/ShowActionTest.java
new file mode 100644 (file)
index 0000000..9cd3054
--- /dev/null
@@ -0,0 +1,279 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.securityreport.ws;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.permission.GroupPermissionDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.component.TestComponentFinder;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.StartupIndexer;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.permission.index.AuthorizationTypeSupport;
+import org.sonar.server.permission.index.PermissionIndexer;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.rules.ExpectedException.none;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.issue.IssueTesting.newIssue;
+import static org.sonar.server.tester.UserSessionRule.standalone;
+import static org.sonar.test.JsonAssert.assertJson;
+
+public class ShowActionTest {
+
+  @Rule
+  public UserSessionRule userSessionRule = standalone();
+  @Rule
+  public DbTester db = DbTester.create();
+  @Rule
+  public EsTester es = EsTester.create();
+  @Rule
+  public ExpectedException expectedException = none();
+
+  private DbClient dbClient = db.getDbClient();
+  private DbSession session = db.getSession();
+  private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule));
+  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient));
+  private WsActionTester ws = new WsActionTester(new ShowAction(userSessionRule, TestComponentFinder.from(db), issueIndex, dbClient));
+  private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
+
+  private UserDto user;
+
+  @Before
+  public void setUp() {
+    user = db.users().insertUser("john");
+    userSessionRule.logIn(user);
+  }
+
+  @Test
+  public void test_definition() {
+    WebService.Action def = ws.getDef();
+    assertThat(def.key()).isEqualTo("show");
+    assertThat(def.isInternal()).isTrue();
+    assertThat(def.isPost()).isFalse();
+    assertThat(def.since()).isEqualTo("7.3");
+
+    assertThat(def.params()).extracting("key").containsExactlyInAnyOrder("standard", "project", "branch", "includeDistribution");
+  }
+
+
+  @Test
+  public void owasp_empty() {
+
+    ComponentDto project = insertComponent(ComponentTesting.newPrivateProjectDto(db.getDefaultOrganization(), "PROJECT_ID").setDbKey("PROJECT_KEY"));
+    userSessionRule.addProjectPermission(UserRole.USER, project);
+    indexPermissions();
+    ComponentDto file = insertComponent(newFileDto(project, null, "FILE_ID").setDbKey("FILE_KEY"));
+    RuleDefinitionDto rule = newRule();
+    IssueDto issue1 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.CODE_SMELL);
+    IssueDto issue2 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.BUG);
+    IssueDto issue3 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.CODE_SMELL);
+    dbClient.issueDao().insert(session, issue1, issue2, issue3);
+    session.commit();
+    indexIssues();
+
+    assertJson(ws.newRequest()
+      .setParam("standard", "owaspTop10")
+      .setParam("project", project.getKey())
+      .execute().getInput())
+      .withStrictArrayOrder()
+      .isSimilarTo(this.getClass().getResource("ShowActionTest/empty.json"));
+  }
+
+  @Test
+  public void owasp_without_cwe() {
+
+    ComponentDto project = insertComponent(ComponentTesting.newPrivateProjectDto(db.getDefaultOrganization(), "PROJECT_ID").setDbKey("PROJECT_KEY"));
+    userSessionRule.addProjectPermission(UserRole.USER, project);
+    indexPermissions();
+    ComponentDto file = insertComponent(newFileDto(project, null, "FILE_ID").setDbKey("FILE_KEY"));
+    RuleDefinitionDto rule = newRule();
+    IssueDto issue1 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.VULNERABILITY);
+    IssueDto issue2 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    IssueDto issue3 = newIssue(rule, project, file)
+      .setStatus(Issue.STATUS_RESOLVED)
+      .setResolution(Issue.RESOLUTION_FIXED)
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    IssueDto issue4 = newIssue(rule, project, file)
+      .setStatus(Issue.STATUS_RESOLVED)
+      .setResolution(Issue.RESOLUTION_WONT_FIX)
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    dbClient.issueDao().insert(session, issue1, issue2, issue3, issue4);
+    session.commit();
+    indexIssues();
+
+    assertJson(ws.newRequest()
+      .setParam("standard", "owaspTop10")
+      .setParam("project", project.getKey())
+      .execute().getInput())
+      .withStrictArrayOrder()
+      .isSimilarTo(this.getClass().getResource("ShowActionTest/owaspNoCwe.json"));
+  }
+
+  @Test
+  public void owasp_with_cwe() {
+
+    ComponentDto project = insertComponent(ComponentTesting.newPrivateProjectDto(db.getDefaultOrganization(), "PROJECT_ID").setDbKey("PROJECT_KEY"));
+    userSessionRule.addProjectPermission(UserRole.USER, project);
+    indexPermissions();
+    ComponentDto file = insertComponent(newFileDto(project, null, "FILE_ID").setDbKey("FILE_KEY"));
+    RuleDefinitionDto rule = newRule();
+    IssueDto issue1 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.VULNERABILITY);
+    IssueDto issue2 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    IssueDto issue3 = newIssue(rule, project, file)
+      .setStatus(Issue.STATUS_RESOLVED)
+      .setResolution(Issue.RESOLUTION_FIXED)
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    IssueDto issue4 = newIssue(rule, project, file)
+      .setStatus(Issue.STATUS_RESOLVED)
+      .setResolution(Issue.RESOLUTION_WONT_FIX)
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    dbClient.issueDao().insert(session, issue1, issue2, issue3, issue4);
+    session.commit();
+    indexIssues();
+
+    assertJson(ws.newRequest()
+      .setParam("standard", "owaspTop10")
+      .setParam("project", project.getKey())
+      .setParam("includeDistribution", "true")
+      .execute().getInput())
+      .withStrictArrayOrder()
+      .isSimilarTo(this.getClass().getResource("ShowActionTest/owaspWithCwe.json"));
+  }
+
+  @Test
+  public void sans_with_cwe() {
+
+    ComponentDto project = insertComponent(ComponentTesting.newPrivateProjectDto(db.getDefaultOrganization(), "PROJECT_ID").setDbKey("PROJECT_KEY"));
+    userSessionRule.addProjectPermission(UserRole.USER, project);
+    indexPermissions();
+    ComponentDto file = insertComponent(newFileDto(project, null, "FILE_ID").setDbKey("FILE_KEY"));
+    RuleDefinitionDto rule = newRule();
+    IssueDto issue1 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.VULNERABILITY);
+    IssueDto issue2 = newIssue(rule, project, file)
+      .setStatus("OPEN")
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    IssueDto issue3 = newIssue(rule, project, file)
+      .setStatus(Issue.STATUS_RESOLVED)
+      .setResolution(Issue.RESOLUTION_FIXED)
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    IssueDto issue4 = newIssue(rule, project, file)
+      .setStatus(Issue.STATUS_RESOLVED)
+      .setResolution(Issue.RESOLUTION_WONT_FIX)
+      .setSeverity("MAJOR")
+      .setType(RuleType.SECURITY_HOTSPOT);
+    dbClient.issueDao().insert(session, issue1, issue2, issue3, issue4);
+    session.commit();
+    indexIssues();
+
+    assertJson(ws.newRequest()
+      .setParam("standard", "sansTop25")
+      .setParam("project", project.getKey())
+      .setParam("includeDistribution", "true")
+      .execute().getInput())
+      .withStrictArrayOrder()
+      .isSimilarTo(this.getClass().getResource("ShowActionTest/sansWithCwe.json"));
+  }
+
+
+  private RuleDefinitionDto newRule() {
+    RuleDefinitionDto rule = RuleTesting.newRule()
+      .setSecurityStandards(new HashSet<>(Arrays.asList("owaspTop10:a2", "cwe:123", "cwe:89")));
+    db.rules().insert(rule);
+    return rule;
+  }
+
+  private void indexPermissions() {
+    permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
+  }
+
+  private void indexIssues() {
+    issueIndexer.indexOnStartup(issueIndexer.getIndexTypes());
+  }
+
+  private void grantPermissionToAnyone(ComponentDto project, String permission) {
+    dbClient.groupPermissionDao().insert(session,
+      new GroupPermissionDto()
+        .setOrganizationUuid(project.getOrganizationUuid())
+        .setGroupId(null)
+        .setResourceId(project.getId())
+        .setRole(permission));
+    session.commit();
+    userSessionRule.logIn().addProjectPermission(permission, project);
+  }
+
+  private ComponentDto insertComponent(ComponentDto component) {
+    dbClient.componentDao().insert(session, component);
+    session.commit();
+    return component;
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/empty.json b/server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/empty.json
new file mode 100644 (file)
index 0000000..ace78e9
--- /dev/null
@@ -0,0 +1,92 @@
+{
+  "categories": [
+    {
+      "category": "a1",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a2",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a3",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a4",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a5",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a6",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a7",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a8",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a9",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a10",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "unknown",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    }
+  ]
+}
\ No newline at end of file
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/owaspNoCwe.json b/server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/owaspNoCwe.json
new file mode 100644 (file)
index 0000000..4e80d47
--- /dev/null
@@ -0,0 +1,93 @@
+{
+  "categories": [
+    {
+      "category": "a1",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a2",
+      "vulnerabilities": 1,
+      "vulnerabilityRating": 3,
+      "toReviewSecurityHotspots": 1,
+      "openSecurityHotspots": 1,
+      "wontFixSecurityHotspots": 1,
+      "distribution": []
+    },
+    {
+      "category": "a3",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a4",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a5",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a6",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a7",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a8",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a9",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a10",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "unknown",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    }
+  ]
+}
\ No newline at end of file
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/owaspWithCwe.json b/server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/owaspWithCwe.json
new file mode 100644 (file)
index 0000000..00444e7
--- /dev/null
@@ -0,0 +1,110 @@
+{
+  "categories": [
+    {
+      "category": "a1",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a2",
+      "vulnerabilities": 1,
+      "vulnerabilityRating": 3,
+      "toReviewSecurityHotspots": 1,
+      "openSecurityHotspots": 1,
+      "wontFixSecurityHotspots": 1,
+      "distribution": [
+        {
+          "cwe": "89",
+          "vulnerabilities": 1,
+          "vulnerabilityRating": 3,
+          "toReviewSecurityHotspots": 1,
+          "openSecurityHotspots": 1,
+          "wontFixSecurityHotspots": 1
+        },
+        {
+          "cwe": "123",
+          "vulnerabilities": 1,
+          "vulnerabilityRating": 3,
+          "toReviewSecurityHotspots": 1,
+          "openSecurityHotspots": 1,
+          "wontFixSecurityHotspots": 1
+        }
+      ]
+    },
+    {
+      "category": "a3",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a4",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a5",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a6",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a7",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a8",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a9",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "a10",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "unknown",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    }
+  ]
+}
\ No newline at end of file
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/sansWithCwe.json b/server/sonar-server/src/test/resources/org/sonar/server/securityreport/ws/ShowActionTest/sansWithCwe.json
new file mode 100644 (file)
index 0000000..fc07774
--- /dev/null
@@ -0,0 +1,46 @@
+{
+  "categories": [
+    {
+      "category": "porous-defenses",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "risky-resource",
+      "vulnerabilities": 0,
+      "toReviewSecurityHotspots": 0,
+      "openSecurityHotspots": 0,
+      "wontFixSecurityHotspots": 0,
+      "distribution": []
+    },
+    {
+      "category": "insecure-interaction",
+      "vulnerabilities": 1,
+      "vulnerabilityRating": 3,
+      "toReviewSecurityHotspots": 1,
+      "openSecurityHotspots": 1,
+      "wontFixSecurityHotspots": 1,
+      "distribution": [
+        {
+          "cwe": "89",
+          "vulnerabilities": 1,
+          "vulnerabilityRating": 3,
+          "toReviewSecurityHotspots": 1,
+          "openSecurityHotspots": 1,
+          "wontFixSecurityHotspots": 1
+        },
+        {
+          "cwe": "123",
+          "vulnerabilities": 1,
+          "vulnerabilityRating": 3,
+          "toReviewSecurityHotspots": 1,
+          "openSecurityHotspots": 1,
+          "wontFixSecurityHotspots": 1
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/sonar-ws/src/main/protobuf/ws-security.proto b/sonar-ws/src/main/protobuf/ws-security.proto
new file mode 100644 (file)
index 0000000..5e3a970
--- /dev/null
@@ -0,0 +1,53 @@
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2016 SonarSource
+// mailto:contact AT sonarsource DOT com
+//
+// SonarQube is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 3 of the License, or (at your option) any later version.
+//
+// SonarQube is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+syntax = "proto2";
+
+package sonarqube.ws.issues;
+
+import "ws-commons.proto";
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "SecurityReports";
+option optimize_for = SPEED;
+
+// Response of GET api/security_reports/show
+message ShowWsResponse {
+  repeated SecurityStandardCategoryStatistics categories = 6;
+}
+
+message SecurityStandardCategoryStatistics {
+  optional string category = 1;
+  optional int64 vulnerabilities = 2;
+  optional int64 vulnerabilityRating = 3;
+  optional int64 toReviewSecurityHotspots = 4;
+  optional int64 openSecurityHotspots = 5;
+  optional int64 wontFixSecurityHotspots = 6;
+  repeated CweStatistics distribution = 7;
+}
+
+message CweStatistics {
+  optional string cwe = 1;
+  optional int64 vulnerabilities = 2;
+  optional int64 vulnerabilityRating = 3;
+  optional int64 toReviewSecurityHotspots = 4;
+  optional int64 openSecurityHotspots = 5;
+  optional int64 wontFixSecurityHotspots = 6;
+}
+
+