diff --git a/checkov/openapi/checks/resource/generic/ClearTextAPIKey.py b/checkov/openapi/checks/resource/generic/ClearTextAPIKey.py new file mode 100644 index 00000000000..9575799e113 --- /dev/null +++ b/checkov/openapi/checks/resource/generic/ClearTextAPIKey.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Any +from checkov.common.models.enums import CheckResult, CheckCategories +from checkov.common.checks.enums import BlockType +from checkov.common.util.consts import LINE_FIELD_NAMES +from checkov.openapi.checks.base_openapi_check import BaseOpenapiCheck + + +class ClearTestAPIKey(BaseOpenapiCheck): + def __init__(self) -> None: + id = "CKV_OPENAPI_20" + name = "Ensure that API keys are not sent over cleartext" + categories = (CheckCategories.API_SECURITY,) + supported_resources = ('paths',) + super().__init__(name=name, id=id, categories=categories, supported_entities=supported_resources, + block_type=BlockType.DOCUMENT) + + def scan_entity_conf(self, conf: dict[str, Any], entity_type: str) -> tuple[CheckResult, dict[str, Any]]: # type:ignore[override] # return type is different than the base class + components = conf.get("components") + security_def = conf.get("securityDefinitions") + if components and isinstance(components, dict): + security_schemes = components.get("securitySchemes") or {} + elif security_def: + security_schemes = security_def + else: + return CheckResult.PASSED, conf + + paths = conf.get('paths') + if not isinstance(paths, dict): + return CheckResult.PASSED, security_schemes + + filtered_dict = {} + if isinstance(security_schemes, dict): + for name, scheme in security_schemes.items(): + if isinstance(scheme, dict) and scheme.get('type') == "apiKey": + filtered_dict[name] = scheme + + if not filtered_dict: + return CheckResult.PASSED, security_schemes + + for key, path in paths.items(): + if not path: + continue + if key in LINE_FIELD_NAMES: + continue + for value in path.values(): + if not isinstance(value, dict): + continue + operation_security = value.get('security') + if operation_security and isinstance(operation_security, list): + for sec in operation_security[0]: + if sec in filtered_dict: + return CheckResult.FAILED, security_schemes + + return CheckResult.PASSED, conf + + +check = ClearTestAPIKey() diff --git a/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail.json b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail.json new file mode 100644 index 00000000000..8c61adf363f --- /dev/null +++ b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail.json @@ -0,0 +1,46 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Simple API overview", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "description": "Creates a new pet in the store", + "responses": { + "200": { + "description": "200 response" + } + }, + "operationId": "addPet", + "security": [ + { + "apiKey1": [], + "apiKey2": [], + "apiKey3": [] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "apiKey1": { + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + }, + "apiKey2": { + "type": "apiKey", + "name": "X-API-Key", + "in": "cookie" + }, + "apiKey3": { + "type": "apiKey", + "name": "X-API-Key", + "in": "query" + } + } + } +} diff --git a/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail.yaml b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail.yaml new file mode 100644 index 00000000000..42b2ebfe522 --- /dev/null +++ b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.0 +info: + title: Simple API overview + version: 1.0.0 +paths: + /pets: + post: + description: Creates a new pet in the store + responses: + "200": + description: 200 response + operationId: addPet + security: + - apiKey1: [] + apiKey2: [] + apiKey3: [] +components: + securitySchemes: + apiKey1: + type: apiKey + name: X-API-Key + in: header + apiKey2: + type: apiKey + name: X-API-Key + in: cookie + apiKey3: + type: apiKey + name: X-API-Key + in: query diff --git a/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail2.json b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail2.json new file mode 100644 index 00000000000..c52bc9aff00 --- /dev/null +++ b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail2.json @@ -0,0 +1,38 @@ +{ + "swagger": "2.0", + "info": { + "title": "Simple API overview", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "description": "Creates a new pet in the store", + "responses": { + "200": { + "description": "200 response" + } + }, + "operationId": "addPet", + "security": [ + { + "apiKey1": [], + "apiKey3": [] + } + ] + } + } + }, + "securityDefinitions": { + "apiKey1": { + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + }, + "apiKey3": { + "type": "apiKey", + "name": "X-API-Key", + "in": "query" + } + } +} diff --git a/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail2.yaml b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail2.yaml new file mode 100644 index 00000000000..a5472498931 --- /dev/null +++ b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/fail2.yaml @@ -0,0 +1,24 @@ +swagger: "2.0" +info: + title: Simple API overview + version: 1.0.0 +paths: + /pets: + post: + description: Creates a new pet in the store + responses: + "200": + description: 200 response + operationId: addPet + security: + - apiKey1: [] + apiKey3: [] +securityDefinitions: + apiKey1: + type: apiKey + name: X-API-Key + in: header + apiKey3: + type: apiKey + name: X-API-Key + in: query diff --git a/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass.json b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass.json new file mode 100644 index 00000000000..6996f58e752 --- /dev/null +++ b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass.json @@ -0,0 +1,45 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Simple API overview" + }, + "paths": { + "/pets": { + "post": { + "description": "Creates a new pet in the store", + "responses": { + "200": { + "description": "200 response" + } + }, + "operationId": "addPet", + "security": [ + { + "OAuth2": [ + "write", + "read" + ] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "scopes": { + "write": "modify objects in your account", + "read": "read objects in your account" + }, + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token" + } + } + } + } + } +} diff --git a/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass.yaml b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass.yaml new file mode 100644 index 00000000000..5da15e32d1c --- /dev/null +++ b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.0 +info: + title: Simple API overview + version: 1.0.0 +paths: + /pets: + post: + description: Creates a new pet in the store + responses: + "200": + description: 200 response + operationId: addPet + security: + - OAuth2: + - write + - read +components: + securitySchemes: + OAuth2: + type: oauth2 + flows: + authorizationCode: + scopes: + write: modify objects in your account + read: read objects in your account + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token diff --git a/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass2.json b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass2.json new file mode 100644 index 00000000000..5349a3d0eb3 --- /dev/null +++ b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass2.json @@ -0,0 +1,40 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Simple API overview" + }, + "paths": { + "/pets": { + "post": { + "description": "Creates a new pet in the store", + "responses": { + "200": { + "description": "200 response" + } + }, + "operationId": "addPet", + "security": [ + { + "OAuth2": [ + "write", + "read" + ] + } + ] + } + } + }, + "securityDefinitions": { + "OAuth2": { + "type": "oauth2", + "flow": "accessCode", + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "read": "Grants read access", + "write": "Grants write access" + } + } + } +} diff --git a/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass2.yaml b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass2.yaml new file mode 100644 index 00000000000..7771f5f5a30 --- /dev/null +++ b/tests/openapi/checks/resource/generic/example_ClearTextAPIKey/pass2.yaml @@ -0,0 +1,25 @@ +swagger: "2.0" +info: + title: Simple API overview + version: 1.0.0 +paths: + /pets: + post: + description: Creates a new pet in the store + responses: + "200": + description: 200 response + operationId: addPet + security: + - OAuth2: + - write + - read +securityDefinitions: + OAuth2: + type: oauth2 + flow: accessCode + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: + read: Grants read access + write: Grants write access diff --git a/tests/openapi/checks/resource/generic/test_ClearTextAPIKey.py b/tests/openapi/checks/resource/generic/test_ClearTextAPIKey.py new file mode 100644 index 00000000000..92f33e2ab0d --- /dev/null +++ b/tests/openapi/checks/resource/generic/test_ClearTextAPIKey.py @@ -0,0 +1,47 @@ +import os +import unittest + +from checkov.openapi.checks.resource.generic.ClearTextAPIKey import check +from checkov.openapi.runner import Runner +from checkov.runner_filter import RunnerFilter + + +class TestClearTextAPIKey(unittest.TestCase): + def test_summary(self): + # given + current_dir = os.path.dirname(os.path.realpath(__file__)) + test_files_dir = current_dir + "/example_ClearTextAPIKey" + + # when + report = Runner().run(root_folder=str(test_files_dir), runner_filter=RunnerFilter(checks=[check.id])) + + # then + summary = report.get_summary() + + passing_resources = { + "/pass.yaml", + "/pass.json", + "/pass2.yaml", + "/pass2.json", + } + failing_resources = { + "/fail.yaml", + "/fail.json", + "/fail2.yaml", + "/fail2.json", + } + + passed_check_resources = {c.file_path for c in report.passed_checks} + failed_check_resources = {c.file_path for c in report.failed_checks} + + self.assertEqual(summary["passed"], len(passing_resources)) + self.assertEqual(summary["failed"], len(failing_resources)) + self.assertEqual(summary["skipped"], 0) + self.assertEqual(summary["parsing_errors"], 0) + + self.assertEqual(passing_resources, passed_check_resources) + self.assertEqual(failing_resources, failed_check_resources) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file