From e9d01cb01f050660b86feeb006f68fcb1593f638 Mon Sep 17 00:00:00 2001 From: Aldo Torres Date: Fri, 30 May 2025 13:40:36 -0500 Subject: [PATCH 1/2] feat: adding new rule, response headers validation --- .../OAR114HttpResponseHeadersChecks.java | 138 ++++++++++++++++++ .../rules/openapi/security/OAR114.html | 82 +++++++++++ .../rules/openapi/security/OAR114.json | 13 ++ .../OAR114HttpResponseHeadersChecksTest.java | 62 ++++++++ .../checks/v2/security/OAR114/valid.json | 32 ++++ .../checks/v2/security/OAR114/valid.yaml | 19 +++ .../OAR114/with-forbidden-params.json | 26 ++++ .../OAR114/with-forbidden-params.yaml | 15 ++ .../OAR114/without-required-params.json | 26 ++++ .../OAR114/without-required-params.yaml | 15 ++ .../checks/v3/security/OAR114/valid.json | 32 ++++ .../checks/v3/security/OAR114/valid.yaml | 19 +++ .../OAR114/with-forbidden-params.json | 26 ++++ .../OAR114/with-forbidden-params.yaml | 15 ++ .../OAR114/without-required-params.json | 26 ++++ .../OAR114/without-required-params.yaml | 15 ++ 16 files changed, 561 insertions(+) create mode 100644 src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java create mode 100644 src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.html create mode 100644 src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.json create mode 100644 src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java create mode 100644 src/test/resources/checks/v2/security/OAR114/valid.json create mode 100644 src/test/resources/checks/v2/security/OAR114/valid.yaml create mode 100644 src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json create mode 100644 src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml create mode 100644 src/test/resources/checks/v2/security/OAR114/without-required-params.json create mode 100644 src/test/resources/checks/v2/security/OAR114/without-required-params.yaml create mode 100644 src/test/resources/checks/v3/security/OAR114/valid.json create mode 100644 src/test/resources/checks/v3/security/OAR114/valid.yaml create mode 100644 src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json create mode 100644 src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml create mode 100644 src/test/resources/checks/v3/security/OAR114/without-required-params.json create mode 100644 src/test/resources/checks/v3/security/OAR114/without-required-params.yaml diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java new file mode 100644 index 00000000..7782d00b --- /dev/null +++ b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java @@ -0,0 +1,138 @@ +package apiaddicts.sonar.openapi.checks.security; + +import apiaddicts.sonar.openapi.checks.BaseCheck; +import com.google.common.collect.ImmutableSet; +import com.sonar.sslr.api.AstNodeType; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apiaddicts.apitools.dosonarapi.api.v2.OpenApi2Grammar; +import org.apiaddicts.apitools.dosonarapi.api.v3.OpenApi3Grammar; +import org.apiaddicts.apitools.dosonarapi.api.v31.OpenApi31Grammar; +import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; + +@Rule(key = OAR114HttpResponseHeadersChecks.KEY) +public class OAR114HttpResponseHeadersChecks extends BaseCheck{ + + public static final String KEY = "OAR114"; + private static final String MANDATORY_HEADERS = "x-api-key"; + private static final String ALLOWED_HEADERS = "x-api-key, traceId, dateTime"; + // private static final String FORBIDDEN_HEADERS = "Accept, Content-Type, Authorization"; + + @RuleProperty( + key = "mandatory-headers", + description = "List of mandatory headers. Comma separated", + defaultValue = MANDATORY_HEADERS + ) + private String mandatoryHeadersStr = MANDATORY_HEADERS; + + @RuleProperty( + key = "allowed-headers", + description = "List of allowed headers. Comma separated", + defaultValue = ALLOWED_HEADERS + ) + private String allowedHeadersStr = ALLOWED_HEADERS; + + // @RuleProperty( + // key = "forbidden-headers", + // description = "List of forbidden headers. Comma separated", + // defaultValue = FORBIDDEN_HEADERS + // ) + // private String forbiddenHeadersStr = FORBIDDEN_HEADERS; + + + private Set mandatoryHeaders = new HashSet<>(); + private Set allowedHeaders = new HashSet<>(); + // private Set forbiddenHeaders = new HashSet<>(); + + @Override + protected void visitFile(JsonNode root) { + if (!mandatoryHeadersStr.trim().isEmpty()) mandatoryHeaders.addAll(Stream.of(mandatoryHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet())); + if (!allowedHeadersStr.trim().isEmpty()) allowedHeaders.addAll(Stream.of(allowedHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet())); + // if (!forbiddenHeadersStr.trim().isEmpty()) forbiddenHeaders.addAll(Stream.of(forbiddenHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet())); + } + + @Override + public Set subscribedKinds() { + return ImmutableSet.of(OpenApi2Grammar.RESPONSE, OpenApi3Grammar.RESPONSE, OpenApi31Grammar.RESPONSE); + } + + @Override + public void visitNode(JsonNode node) { + // if (node.getType() == OpenApi2Grammar.RESPONSE || + // node.getType() == OpenApi3Grammar.RESPONSE || + // node.getType() == OpenApi31Grammar.RESPONSE) { + // validateResponseHeaders(node); + // } + + validateResponseHeaders(node); + + } + + // private void visitPathV2Node(JsonNode node) { + // Collection operationNodes = node.properties().stream().filter(propertyNode -> isOperation(propertyNode)).collect(Collectors.toList()); + // for (JsonNode operationNode : operationNodes) { + // JsonNode parametersNode = operationNode.get("parameters"); + // List headerNames = listHeaderParameters(parametersNode); + // if (mandatoryHeaders == null || mandatoryHeaders.isEmpty()) return; + // if (!headerNames.containsAll(mandatoryHeaders)) { + // addIssue(KEY, translate("generic.mandatory-headers", mandatoryHeadersStr), operationNode.key()); + // } + // } + // } + + private void validateResponseHeaders(JsonNode node) { + JsonNode headersNode = node.get("headers"); + + if (headersNode == null || headersNode.isMissing() || headersNode.isNull()) return; + + List headerDefinitions = new ArrayList<>(headersNode.properties()); + List headerNames = new ArrayList<>(); + + for (JsonNode headerDef : headerDefinitions) { + String headerName = headerDef.key().getTokenValue().toLowerCase().trim(); + headerNames.add(headerName); + + // if (forbiddenHeaders.contains(headerName)) { + // addIssue(KEY, translate("generic.forbidden-response-header", headerName), headerDef.key()); + // } + + if (!allowedHeaders.isEmpty() && !allowedHeaders.contains(headerName)) { + addIssue(KEY, translate("generic.not-allowed-header", headerName), headerDef.key()); + } + } + if (mandatoryHeaders != null && !mandatoryHeaders.isEmpty() && + !headerNames.containsAll(mandatoryHeaders)) { + addIssue(KEY, translate("generic.mandatory-headers", mandatoryHeadersStr), node.key()); + } + } + + // private List listHeaderParameters(JsonNode parametersNode) { + // if (parametersNode.isMissing() || parametersNode.isNull()) return new ArrayList(); + // List headerParametersNodes = parametersNode.elements().stream().filter(this::isHeaderParam).collect(Collectors.toList()); + // if (headerParametersNodes.isEmpty()) return new ArrayList(); + // for (JsonNode headerParameterNode : headerParametersNodes) { + // JsonNode headerNameNode = headerParameterNode.resolve().get("name"); + // String headerName = headerNameNode.getTokenValue().toLowerCase().trim(); + // if (!allowedHeaders.contains(headerName) || forbiddenHeaders.contains(headerName)) { + // addIssue(KEY, translate("generic.not-allowed-header"), headerNameNode.value()); + // } + // JsonNode requiredProperty = headerParameterNode.resolve().get("required"); + // if (mandatoryHeaders != null && !mandatoryHeaders.isEmpty() && mandatoryHeaders.contains(headerName) + // && ( requiredProperty.isMissing() || requiredProperty.isNull() || !Boolean.parseBoolean(requiredProperty.getTokenValue()) ) + // ) { + // addIssue(KEY, translate("OAR033.error-header-required", headerParameterNode.resolve().get("name").getTokenValue()), headerNameNode.value()); + // } + // } + // return headerParametersNodes.stream().map(headerNode -> headerNode.resolve().get("name").getTokenValue().toLowerCase().trim()).collect(Collectors.toList()); + // } + + // private boolean isHeaderParam(JsonNode n) { + // return n.resolve().at("/in").getTokenValue().equals("header"); + // } +} diff --git a/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.html b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.html new file mode 100644 index 00000000..bc606f00 --- /dev/null +++ b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.html @@ -0,0 +1,82 @@ +

Normative - API Definition

+

Overriding certain headers or allowing any headers to be set and not specifying required headers can cause some vulnerabilities in the API.

+

Noncompliant Code Example (OpenAPI 2)

+
+  swagger: "2.0"
+  info:
+    version: 1.0.0
+    title: Swagger Petstore
+  paths:
+    /pets:
+      get:
+        responses:
+          200:
+            description: Ok
+            headers: 
+              Authorization: # Noncompliant {{OAR033: Header not allowed}}
+                description: Forbidden header
+                schema:
+                  type: string
+
+

Compliant Solution (OpenAPI 2)

+
+swagger: "2.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+paths:
+  /pets:
+    get:
+      responses:
+        200:
+          description: Ok
+          headers:
+            x-api-key:
+              description: Mandatory header
+              schema:
+                type: string
+            traceId:
+              description: Optional but allowed
+              schema:
+                type: string
+
+

Noncompliant Code Example (OpenAPI 3)

+
+  openapi: "3.0.0"
+  info:
+    version: 1.0.0
+    title: Swagger Petstore
+  paths:
+    /pets:
+      get:
+        responses:
+          200:
+            description: Ok
+             headers: 
+              Authorization: # Noncompliant {{OAR033: Header not allowed}}
+                description: Forbidden header
+                schema:
+                  type: string
+
+

Compliant Solution (OpenAPI 3)

+
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+paths:
+  /pets:
+    get:
+      responses:
+        200:
+          description: Ok
+          headers:
+            x-api-key:
+              description: Mandatory header
+              schema:
+                type: string
+            traceId:
+              description: Optional but allowed
+              schema:
+                type: string
+
\ No newline at end of file diff --git a/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.json b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.json new file mode 100644 index 00000000..bfb2c510 --- /dev/null +++ b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.json @@ -0,0 +1,13 @@ +{ + "title": "OAR114 - HttpResponseHeaders - There are mandatory request headers and others that are not allowed", + "type": "VULNERABILITY", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "15min" + }, + "tags": [ + "safety" + ], + "defaultSeverity": "CRITICAL" +} \ No newline at end of file diff --git a/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java b/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java new file mode 100644 index 00000000..1e48416d --- /dev/null +++ b/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java @@ -0,0 +1,62 @@ +package org.sonar.samples.openapi.checks.security; +import apiaddicts.sonar.openapi.checks.security.OAR114HttpResponseHeadersChecks; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.api.server.rule.RuleParamType; +import org.sonar.samples.openapi.BaseCheckTest; + +public class OAR114HttpResponseHeadersChecksTest extends BaseCheckTest { +@Before + public void init() { + ruleName = "OAR114"; + check = new OAR114HttpResponseHeadersChecks(); + v2Path = getV2Path("security"); + v3Path = getV3Path("security"); + } + + @Test + public void verifyInV2() { + verifyV2("valid"); + } + + @Test + public void verifyInV2WithForbiddenParams() { + verifyV2("with-forbidden-params"); + } + + @Test + public void verifyInV2WithoutRequiredParams() { + verifyV2("without-required-params"); + } + + @Test + public void verifyInV3() { + verifyV3("valid"); + } + + @Test + public void verifyInV3WithForbiddenParams() { + verifyV3("with-forbidden-params"); + } + + @Test + public void verifyInV3WithoutRequiredParams() { + verifyV3("without-required-params"); + } + + @Override + public void verifyRule() { + assertRuleProperties("OAR114 - HttpResponseHeaders - There are mandatory request headers and others that are not allowed", RuleType.VULNERABILITY, Severity.CRITICAL, tags("safety")); + } + + @Override + public void verifyParameters() { + assertNumberOfParameters(4); + assertParameterProperties("mandatory-headers", "x-api-key", RuleParamType.STRING); + assertParameterProperties("forbidden-headers", "Accept, Content-Type, Authorization", RuleParamType.STRING); + // assertParameterProperties("allowed-headers", "x-api-key, traceId, dateTime", RuleParamType.STRING); + // assertParameterProperties("path-exclusions", "/status", RuleParamType.STRING); + } +} diff --git a/src/test/resources/checks/v2/security/OAR114/valid.json b/src/test/resources/checks/v2/security/OAR114/valid.json new file mode 100644 index 00000000..c1dfba64 --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/valid.json @@ -0,0 +1,32 @@ +{ + "swagger": "2.0", + "info": { + "title": "Valid Response Headers Test", + "version": "1.0" + }, + "paths": { + "/example": { + "get": { + "responses": { + "200": { + "description": "OK", + "headers": { + "x-api-key": { + "description": "Mandatory header", + "schema": { + "type": "string" + } + }, + "traceId": { + "description": "Optional but allowed", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v2/security/OAR114/valid.yaml b/src/test/resources/checks/v2/security/OAR114/valid.yaml new file mode 100644 index 00000000..6eb95b1d --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/valid.yaml @@ -0,0 +1,19 @@ +swagger: "2.0" +info: + title: Valid Response Headers Test + version: "1.0" +paths: + /example: + get: + responses: + "200": + description: OK + headers: + x-api-key: + description: Mandatory header + schema: + type: string + traceId: + description: Optional but allowed + schema: + type: string diff --git a/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json new file mode 100644 index 00000000..d9bc2a4b --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json @@ -0,0 +1,26 @@ +{ + "swagger": "2.0", + "info": { + "title": "Forbidden Header Test", + "version": "1.0" + }, + "paths": { + "/example": { + "get": { + "responses": { + "200": { + "description": "OK", + "headers": { + "Authorization": { # Noncompliant {{OAR033: Header not allowed}} + "description": "Forbidden header", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml new file mode 100644 index 00000000..e4352c2e --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml @@ -0,0 +1,15 @@ +swagger: "2.0" +info: + title: Forbidden Header Test + version: "1.0" +paths: + /example: + get: + responses: + "200": + description: OK + headers: + Authorization: # Noncompliant {{OAR033: Header not allowed}} + description: Forbidden header + schema: + type: string diff --git a/src/test/resources/checks/v2/security/OAR114/without-required-params.json b/src/test/resources/checks/v2/security/OAR114/without-required-params.json new file mode 100644 index 00000000..b661c051 --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/without-required-params.json @@ -0,0 +1,26 @@ +{ + "swagger": "2.0", + "info": { + "title": "Missing Mandatory Header Test", + "version": "1.0" + }, + "paths": { + "/example": { + "get": { + "responses": { + "200": { # Noncompliant {{OAR033: Headers [x-api-key] are required}} + "description": "OK", + "headers": { + "traceId": { + "description": "Allowed header", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml b/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml new file mode 100644 index 00000000..f340446a --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml @@ -0,0 +1,15 @@ +swagger: "2.0" +info: + title: Missing Mandatory Header Test + version: "1.0" +paths: + /example: + get: + responses: + "200": # Noncompliant {{OAR033: Headers [x-api-key] are required}} + description: OK + headers: + traceId: + description: Allowed header + schema: + type: string diff --git a/src/test/resources/checks/v3/security/OAR114/valid.json b/src/test/resources/checks/v3/security/OAR114/valid.json new file mode 100644 index 00000000..79d9c816 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/valid.json @@ -0,0 +1,32 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Valid Response Headers Test", + "version": "1.0" + }, + "paths": { + "/example": { + "get": { + "responses": { + "200": { + "description": "OK", + "headers": { + "x-api-key": { + "description": "Mandatory header", + "schema": { + "type": "string" + } + }, + "traceId": { + "description": "Optional but allowed", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v3/security/OAR114/valid.yaml b/src/test/resources/checks/v3/security/OAR114/valid.yaml new file mode 100644 index 00000000..8c936a19 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/valid.yaml @@ -0,0 +1,19 @@ +openapi: 3.0.0 +info: + title: Valid Response Headers Test + version: "1.0" +paths: + /example: + get: + responses: + "200": + description: OK + headers: + x-api-key: + description: Mandatory header + schema: + type: string + traceId: + description: Optional but allowed + schema: + type: string diff --git a/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json new file mode 100644 index 00000000..15e06886 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json @@ -0,0 +1,26 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Forbidden Header Test", + "version": "1.0" + }, + "paths": { + "/example": { + "get": { + "responses": { + "200": { + "description": "OK", + "headers": { + "Authorization": { # Noncompliant {{OAR033: Header not allowed}} + "description": "Forbidden header", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml new file mode 100644 index 00000000..e24c2416 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.0 +info: + title: Forbidden Header Test + version: "1.0" +paths: + /example: + get: + responses: + "200": + description: OK + headers: + Authorization: # Noncompliant {{OAR033: Header not allowed}} + description: Forbidden header + schema: + type: string diff --git a/src/test/resources/checks/v3/security/OAR114/without-required-params.json b/src/test/resources/checks/v3/security/OAR114/without-required-params.json new file mode 100644 index 00000000..c3fd2ae0 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/without-required-params.json @@ -0,0 +1,26 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Missing Mandatory Header Test", + "version": "1.0" + }, + "paths": { + "/example": { + "get": { + "responses": { + "200": { # Noncompliant {{OAR033: Headers [x-api-key] are required}} + "description": "OK", + "headers": { + "traceId": { + "description": "Allowed header", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml b/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml new file mode 100644 index 00000000..b14b2b6f --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.0 +info: + title: Missing Mandatory Header Test + version: "1.0" +paths: + /example: + get: + responses: + "200": # Noncompliant {{OAR033: Headers [x-api-key] are required}} + description: OK + headers: + traceId: + description: Allowed header + schema: + type: string From 4f33f51f4e584f7c59c8ce3bc675d4a7669d1369 Mon Sep 17 00:00:00 2001 From: Aldo Torres Date: Mon, 2 Jun 2025 16:36:34 -0500 Subject: [PATCH 2/2] feat: adding new rule, response header, checkss --- .../sonar/openapi/checks/RulesLists.java | 3 +- .../OAR114HttpResponseHeadersChecks.java | 55 ------------------- .../OAR114HttpResponseHeadersChecksTest.java | 6 +- .../checks/v2/security/OAR114/valid.json | 12 ++-- .../checks/v2/security/OAR114/valid.yaml | 6 +- .../OAR114/with-forbidden-params.json | 10 ++-- .../OAR114/with-forbidden-params.yaml | 7 +-- .../OAR114/without-required-params.json | 6 +- .../OAR114/without-required-params.yaml | 5 +- .../OAR114/with-forbidden-params.json | 4 +- .../OAR114/with-forbidden-params.yaml | 4 +- .../OAR114/without-required-params.json | 2 +- .../OAR114/without-required-params.yaml | 2 +- 13 files changed, 27 insertions(+), 95 deletions(-) diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java b/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java index 2657e87d..b61e5be8 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java @@ -92,7 +92,8 @@ public static List> getSecurityChecks() { OAR085OpenAPIVersionCheck.class, OAR096ForbiddenResponseCheck.class, OAR045DefinedResponseCheck.class, - OAR049NoContentIn204Check.class + OAR049NoContentIn204Check.class, + OAR114HttpResponseHeadersChecks.class ); } diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java index 7782d00b..ad4df8b0 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java @@ -22,7 +22,6 @@ public class OAR114HttpResponseHeadersChecks extends BaseCheck{ public static final String KEY = "OAR114"; private static final String MANDATORY_HEADERS = "x-api-key"; private static final String ALLOWED_HEADERS = "x-api-key, traceId, dateTime"; - // private static final String FORBIDDEN_HEADERS = "Accept, Content-Type, Authorization"; @RuleProperty( key = "mandatory-headers", @@ -38,23 +37,14 @@ public class OAR114HttpResponseHeadersChecks extends BaseCheck{ ) private String allowedHeadersStr = ALLOWED_HEADERS; - // @RuleProperty( - // key = "forbidden-headers", - // description = "List of forbidden headers. Comma separated", - // defaultValue = FORBIDDEN_HEADERS - // ) - // private String forbiddenHeadersStr = FORBIDDEN_HEADERS; - private Set mandatoryHeaders = new HashSet<>(); private Set allowedHeaders = new HashSet<>(); - // private Set forbiddenHeaders = new HashSet<>(); @Override protected void visitFile(JsonNode root) { if (!mandatoryHeadersStr.trim().isEmpty()) mandatoryHeaders.addAll(Stream.of(mandatoryHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet())); if (!allowedHeadersStr.trim().isEmpty()) allowedHeaders.addAll(Stream.of(allowedHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet())); - // if (!forbiddenHeadersStr.trim().isEmpty()) forbiddenHeaders.addAll(Stream.of(forbiddenHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet())); } @Override @@ -64,27 +54,9 @@ public Set subscribedKinds() { @Override public void visitNode(JsonNode node) { - // if (node.getType() == OpenApi2Grammar.RESPONSE || - // node.getType() == OpenApi3Grammar.RESPONSE || - // node.getType() == OpenApi31Grammar.RESPONSE) { - // validateResponseHeaders(node); - // } - validateResponseHeaders(node); - } - // private void visitPathV2Node(JsonNode node) { - // Collection operationNodes = node.properties().stream().filter(propertyNode -> isOperation(propertyNode)).collect(Collectors.toList()); - // for (JsonNode operationNode : operationNodes) { - // JsonNode parametersNode = operationNode.get("parameters"); - // List headerNames = listHeaderParameters(parametersNode); - // if (mandatoryHeaders == null || mandatoryHeaders.isEmpty()) return; - // if (!headerNames.containsAll(mandatoryHeaders)) { - // addIssue(KEY, translate("generic.mandatory-headers", mandatoryHeadersStr), operationNode.key()); - // } - // } - // } private void validateResponseHeaders(JsonNode node) { JsonNode headersNode = node.get("headers"); @@ -98,10 +70,6 @@ private void validateResponseHeaders(JsonNode node) { String headerName = headerDef.key().getTokenValue().toLowerCase().trim(); headerNames.add(headerName); - // if (forbiddenHeaders.contains(headerName)) { - // addIssue(KEY, translate("generic.forbidden-response-header", headerName), headerDef.key()); - // } - if (!allowedHeaders.isEmpty() && !allowedHeaders.contains(headerName)) { addIssue(KEY, translate("generic.not-allowed-header", headerName), headerDef.key()); } @@ -112,27 +80,4 @@ private void validateResponseHeaders(JsonNode node) { } } - // private List listHeaderParameters(JsonNode parametersNode) { - // if (parametersNode.isMissing() || parametersNode.isNull()) return new ArrayList(); - // List headerParametersNodes = parametersNode.elements().stream().filter(this::isHeaderParam).collect(Collectors.toList()); - // if (headerParametersNodes.isEmpty()) return new ArrayList(); - // for (JsonNode headerParameterNode : headerParametersNodes) { - // JsonNode headerNameNode = headerParameterNode.resolve().get("name"); - // String headerName = headerNameNode.getTokenValue().toLowerCase().trim(); - // if (!allowedHeaders.contains(headerName) || forbiddenHeaders.contains(headerName)) { - // addIssue(KEY, translate("generic.not-allowed-header"), headerNameNode.value()); - // } - // JsonNode requiredProperty = headerParameterNode.resolve().get("required"); - // if (mandatoryHeaders != null && !mandatoryHeaders.isEmpty() && mandatoryHeaders.contains(headerName) - // && ( requiredProperty.isMissing() || requiredProperty.isNull() || !Boolean.parseBoolean(requiredProperty.getTokenValue()) ) - // ) { - // addIssue(KEY, translate("OAR033.error-header-required", headerParameterNode.resolve().get("name").getTokenValue()), headerNameNode.value()); - // } - // } - // return headerParametersNodes.stream().map(headerNode -> headerNode.resolve().get("name").getTokenValue().toLowerCase().trim()).collect(Collectors.toList()); - // } - - // private boolean isHeaderParam(JsonNode n) { - // return n.resolve().at("/in").getTokenValue().equals("header"); - // } } diff --git a/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java b/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java index 1e48416d..fde1b59c 100644 --- a/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java +++ b/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java @@ -53,10 +53,8 @@ public void verifyRule() { @Override public void verifyParameters() { - assertNumberOfParameters(4); + assertNumberOfParameters(2); assertParameterProperties("mandatory-headers", "x-api-key", RuleParamType.STRING); - assertParameterProperties("forbidden-headers", "Accept, Content-Type, Authorization", RuleParamType.STRING); - // assertParameterProperties("allowed-headers", "x-api-key, traceId, dateTime", RuleParamType.STRING); - // assertParameterProperties("path-exclusions", "/status", RuleParamType.STRING); + assertParameterProperties("allowed-headers", "x-api-key, traceId, dateTime", RuleParamType.STRING); } } diff --git a/src/test/resources/checks/v2/security/OAR114/valid.json b/src/test/resources/checks/v2/security/OAR114/valid.json index c1dfba64..2e73c4f8 100644 --- a/src/test/resources/checks/v2/security/OAR114/valid.json +++ b/src/test/resources/checks/v2/security/OAR114/valid.json @@ -12,16 +12,12 @@ "description": "OK", "headers": { "x-api-key": { - "description": "Mandatory header", - "schema": { - "type": "string" - } + "type": "string", + "description": "Mandatory header" }, "traceId": { - "description": "Optional but allowed", - "schema": { - "type": "string" - } + "type": "string", + "description": "Optional but allowed" } } } diff --git a/src/test/resources/checks/v2/security/OAR114/valid.yaml b/src/test/resources/checks/v2/security/OAR114/valid.yaml index 6eb95b1d..50cf1f9f 100644 --- a/src/test/resources/checks/v2/security/OAR114/valid.yaml +++ b/src/test/resources/checks/v2/security/OAR114/valid.yaml @@ -10,10 +10,8 @@ paths: description: OK headers: x-api-key: + type: string description: Mandatory header - schema: - type: string traceId: + type: string description: Optional but allowed - schema: - type: string diff --git a/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json index d9bc2a4b..6f288ccf 100644 --- a/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json +++ b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json @@ -8,14 +8,12 @@ "/example": { "get": { "responses": { - "200": { + "200": { # Noncompliant {{OAR114: Headers [x-api-key] are required}} "description": "OK", "headers": { - "Authorization": { # Noncompliant {{OAR033: Header not allowed}} - "description": "Forbidden header", - "schema": { - "type": "string" - } + "Authorization": { # Noncompliant {{OAR114: Header not allowed}} + "type": "string", + "description": "Forbidden header" } } } diff --git a/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml index e4352c2e..257c4a53 100644 --- a/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml +++ b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml @@ -6,10 +6,9 @@ paths: /example: get: responses: - "200": + "200": # Noncompliant {{OAR114: Headers [x-api-key] are required}} description: OK headers: - Authorization: # Noncompliant {{OAR033: Header not allowed}} + Authorization: # Noncompliant {{OAR114: Header not allowed}} + type: string description: Forbidden header - schema: - type: string diff --git a/src/test/resources/checks/v2/security/OAR114/without-required-params.json b/src/test/resources/checks/v2/security/OAR114/without-required-params.json index b661c051..3714f087 100644 --- a/src/test/resources/checks/v2/security/OAR114/without-required-params.json +++ b/src/test/resources/checks/v2/security/OAR114/without-required-params.json @@ -8,14 +8,12 @@ "/example": { "get": { "responses": { - "200": { # Noncompliant {{OAR033: Headers [x-api-key] are required}} + "200": { # Noncompliant {{OAR114: Headers [x-api-key] are required}} "description": "OK", "headers": { "traceId": { + "type": "string", "description": "Allowed header", - "schema": { - "type": "string" - } } } } diff --git a/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml b/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml index f340446a..87969337 100644 --- a/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml +++ b/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml @@ -6,10 +6,9 @@ paths: /example: get: responses: - "200": # Noncompliant {{OAR033: Headers [x-api-key] are required}} + "200": # Noncompliant {{OAR114: Headers [x-api-key] are required}} description: OK headers: traceId: + type: string description: Allowed header - schema: - type: string diff --git a/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json index 15e06886..975318bc 100644 --- a/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json +++ b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json @@ -8,10 +8,10 @@ "/example": { "get": { "responses": { - "200": { + "200": { # Noncompliant {{OAR114: Headers [x-api-key] are required}} "description": "OK", "headers": { - "Authorization": { # Noncompliant {{OAR033: Header not allowed}} + "Authorization": { # Noncompliant {{OAR114: Header not allowed}} "description": "Forbidden header", "schema": { "type": "string" diff --git a/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml index e24c2416..4f2c2bc7 100644 --- a/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml +++ b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml @@ -6,10 +6,10 @@ paths: /example: get: responses: - "200": + "200": # Noncompliant {{OAR114: Headers [x-api-key] are required}} description: OK headers: - Authorization: # Noncompliant {{OAR033: Header not allowed}} + Authorization: # Noncompliant {{OAR114: Header not allowed}} description: Forbidden header schema: type: string diff --git a/src/test/resources/checks/v3/security/OAR114/without-required-params.json b/src/test/resources/checks/v3/security/OAR114/without-required-params.json index c3fd2ae0..876746f3 100644 --- a/src/test/resources/checks/v3/security/OAR114/without-required-params.json +++ b/src/test/resources/checks/v3/security/OAR114/without-required-params.json @@ -8,7 +8,7 @@ "/example": { "get": { "responses": { - "200": { # Noncompliant {{OAR033: Headers [x-api-key] are required}} + "200": { # Noncompliant {{OAR114: Headers [x-api-key] are required}} "description": "OK", "headers": { "traceId": { diff --git a/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml b/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml index b14b2b6f..5603c561 100644 --- a/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml +++ b/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml @@ -6,7 +6,7 @@ paths: /example: get: responses: - "200": # Noncompliant {{OAR033: Headers [x-api-key] are required}} + "200": # Noncompliant {{OAR114: Headers [x-api-key] are required}} description: OK headers: traceId: