[CircleCI Security Alert] Rotate any secrets stored in CircleCI

For those working with circleCI and GitHub, here is a script we did today

QuickStart

python main.py list-ctx
python main.py list-env
python main.py list-keys
python main.py replace-keys

with the following main.py file

import requests
import argparse
from config import API_TOKEN, GH_TOKEN, ORG # CircleCi api Token, Github api token, Github organisation name

ORG_SLUG = f"github/{ORG}"

auth_header = { 'Circle-Token': API_TOKEN }

github_headers = {
    "X-GitHub-Api-Version": "2022-11-28",
    "Accept": "application/vnd.github+json",
    "Authorization": f"Bearer {GH_TOKEN}"
}

def list_contexts():
    url = "https://circleci.com/api/v2/context"
    req_params = {
        "owner-slug": ORG_SLUG,
        "owner-type": "organization",
    }

    page_token = ""

    contexts = []

    while True:
        params = {**req_params, "page-token": page_token}
        response = requests.get(url, params=params, headers=auth_header).json()
        contexts += response.get("items", [])
        page_token = response.get("next_page_token", None)

        if page_token is None:
            return contexts

def list_context_variables(context_id):
    url = f"https://circleci.com/api/v2/context/{context_id}/environment-variable"
    
    req_params = {}

    page_token = ""

    env_vars = []

    while True:
        params = {**req_params, "page-token": page_token}
        response = requests.get(url, params=params, headers=auth_header).json()
        env_vars += response.get("items", [])
        page_token = response.get("next_page_token", None)

        if page_token is None:
            return env_vars

def list_github_repositories():
    url = f"https://api.github.com/orgs/{ORG}/repos"

    req_params = {
        "per_page": 100
    }

    page_idx = 1

    repos = []

    while True:
        params = {**req_params, "page": page_idx}
        response = requests.get(url, params=params, headers=github_headers).json()
        repos += response
        if len(response) == 0:
            return repos
        page_idx += 1

def list_project_env_variables(project):
    url = f"https://circleci.com/api/v2/project/gh/{ORG}/{project}/envvar"
    
    req_params = {}

    page_token = ""

    env_vars = []

    while True:
        params = {**req_params, "page-token": page_token}
        response = requests.get(url, params=params, headers=auth_header).json()
        env_vars += response.get("items", [])
        page_token = response.get("next_page_token", None)

        if page_token is None:
            return env_vars

def list_project_keys(project):
    url = f"https://circleci.com/api/v2/project/gh/{ORG}/{project}/checkout-key"
    
    req_params = {}

    page_token = ""

    keys = []

    while True:
        params = {**req_params, "page-token": page_token}
        response = requests.get(url, params=params, headers=auth_header).json()
        keys += response.get("items", [])
        page_token = response.get("next_page_token", None)

        if page_token is None:
            return keys 

def list_github_deploy_keys(project):
    url = f"https://api.github.com/repos/{ORG}/{project}/keys"

    req_params = {
        "per_page": 100
    }

    page_idx = 1

    keys = []

    while True:
        params = {**req_params, "page": page_idx}
        response = requests.get(url, params=params, headers=github_headers).json()
        keys += response
        if len(response) == 0:
            return keys
        page_idx += 1

def delete_github_deploy_key(project, key):
    url = f"https://api.github.com/repos/{ORG}/{project}/keys/{key['id']}"

    response = requests.delete(url, headers=github_headers)

    if response.status_code != 204:
        print("Could not delete github key", key['title'], response.json())


def get_project_checkout_keys(project, fingerprint):
    url = f"https://circleci.com/api/v2/project/gh/{ORG}/{project}/checkout-key/{fingerprint}"

    response = requests.get(url, headers=auth_header).json()

    return response

def delete_project_checkout_keys(project, fingerprint):
    url = f"https://circleci.com/api/v2/project/gh/{ORG}/{project}/checkout-key/{fingerprint}"

    response = requests.delete(url, headers=auth_header)

    if response.status_code != 200:
        print("Could not delete circleci key for project", project) 

def create_project_checkout_keys(project, key_type):
    url = f"https://circleci.com/api/v2/project/gh/{ORG}/{project}/checkout-key"

    response = requests.post(url, headers=auth_header, json={"type": key_type})

    if response.status_code != 201:
        print("Could not create circleci key for project", project, response.json()) 

def main_list_project_env_var():
    repos = list_github_repositories()
    print("project;var")
    for repo in repos:
        env_vars = list_project_env_variables(repo["name"])
        for var in env_vars:
            print(f'{repo["name"]};{var["name"]}')

def main_list_project_checkout_keys():
    repos = list_github_repositories()
    print("project;type;fingerprint;created_at")
    for repo in repos:
        keys = list_project_keys(repo["name"])
        for key in keys:
            print(f'{repo["name"]};{key["type"]};{key["fingerprint"]};{key["created_at"]}')


def main_list_contexts():
    contexts = list_contexts()

    print("context;var;last_modified")
    for ctx in contexts:
        env_vars = list_context_variables(ctx["id"])
        for var in env_vars:
            print(f'{ctx["name"]};{var["variable"]};{var["created_at"]}')

def main_replace_project_checkout_keys():
    repos = list_github_repositories()
    for repo in repos:
        project = repo["name"]
        gh_keys = list_github_deploy_keys(project)

        for key in gh_keys:
            delete_github_deploy_key(project, key)

        ci_keys = list_project_keys(project)
        for key in ci_keys:
            delete_project_checkout_keys(project, key["fingerprint"])
            create_project_checkout_keys(project, key_type=key['type'])
            

command_dict = {
    "list-ctx": main_list_contexts,
    "list-env": main_list_project_env_var,
    "list-keys": main_list_project_checkout_keys,
    "replace-keys": main_replace_project_checkout_keys
}

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("cmd", choices=list(command_dict.keys()))
    args = parser.parse_args()
    
    command_dict[args.cmd]()
2 Likes