Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(openapi): New OpenAPI check CKV_OPENAPI_20 #5253

Merged
merged 9 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions checkov/openapi/checks/resource/generic/ClearTextAPIKey.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions tests/openapi/checks/resource/generic/test_ClearTextAPIKey.py
Original file line number Diff line number Diff line change
@@ -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()