Skip to content

Instantly share code, notes, and snippets.

@lispyclouds
Last active January 8, 2023 10:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lispyclouds/7752a72f388ad5136f3a1d3843ceb9e8 to your computer and use it in GitHub Desktop.
Save lispyclouds/7752a72f388ad5136f3a1d3843ceb9e8 to your computer and use it in GitHub Desktop.
Babashka script to rotate all CircleCI deploy keys in GitHub
(require '[babashka.deps :as deps])
(deps/add-deps '{:deps {org.babashka/http-client {:mvn/version "0.0.2"}}})
(require '[babashka.http-client :as http]
'[cheshire.core :as json])
(import '[java.net URL])
(defn get-projects
"Returns the projects followed by the creator of the token as org/repo"
[token]
(let [urls (-> (http/get "https://circleci.com/api/v1.1/me"
{:headers {"Circle-Token" token
"Accept" "application/json"}})
:body
json/parse-string
(get "projects")
keys)]
(map #(-> %
URL.
.getPath
(subs 1))
urls)))
(defn get-gh-deploy-keys
"Returns a map of the SSH key to its id in a org/repo"
[token project]
(let [dkeys (-> (http/get (format "https://api.github.com/repos/%s/keys" project)
{:headers {"Authorization" (str "Bearer " token)}})
:body
(json/parse-string true))]
(->> dkeys
(map #(vector (% :key)
(% :id)))
(into {}))))
(defn get-circle-checkout-keys
"Returns a map of the SSH key to its fingerprint in a org/repo"
[token project]
(let [pkeys (-> (http/get (format "https://circleci.com/api/v2/project/gh/%s/checkout-key" project)
{:headers {"Circle-Token" token}})
:body
(json/parse-string true)
:items)]
(->> pkeys
(filter #(= "deploy-key" (% :type)))
(map #(vector (% :public_key)
(% :fingerprint)))
(into {}))))
(defn delete-gh-key
"Deletes a key in GitHub"
[token project id]
(http/delete (format "https://api.github.com/repos/%s/keys/%d"
project
id)
{:headers {"Authorization" (str "Bearer " token)}}))
(defn delete-circle-key
"Deletes a key in CircleCI"
[token project fingerprint]
(http/delete (format "https://circleci.com/api/v2/project/gh/%s/checkout-key/%s"
project
fingerprint)
{:headers {"Circle-Token" token}}))
(defn create-deploy-key
"Creates a new deploy key in Circle"
[token project]
(http/post (format "https://circleci.com/api/v2/project/gh/%s/checkout-key" project)
{:headers {"Circle-Token" token
"Content-Type" "application/json"}
:body (json/generate-string {"type" "deploy-key"})}))
(defn make-deletion-data
"Given a map of circle and github tokens, returns a list of matching key info in a org/repo"
[project circle-tokens gh-tokens]
(->> circle-tokens
(map (fn [[k f]]
(when (contains? gh-tokens k)
{:project project
:id (gh-tokens k)
:fingerprint f})))
(filter some?)))
(defn rotate-deploy-keys
"Driver fn, pass dry-run as true to avoid the rotations"
([gh-token circle-token]
(rotate-deploy-keys gh-token circle-token false))
([gh-token circle-token dry-run]
(let [projects (get-projects circle-token)]
(->> projects
(mapcat #(make-deletion-data %
(get-circle-checkout-keys circle-token %)
(get-gh-deploy-keys gh-token %)))
(run! (fn [{:keys [fingerprint id project]}]
(println (format "Rotating key id: %s fingerprint: %s for %s"
id
fingerprint
project))
(when-not dry-run
(delete-circle-key circle-token project fingerprint)
(println "circle deleted")
(delete-gh-key gh-token project id)
(println "gh deleted")
(create-deploy-key circle-token project)
(println "new key generated on circle"))))))))
(when (= *file* (System/getProperty "babashka.file"))
(let [github-token (System/getenv "GITHUB_TOKEN")
circle-token (System/getenv "CIRCLE_TOKEN")]
(when (or (nil? github-token) (nil? circle-token))
(println "Please set GITHUB_TOKEN and CIRCLE_TOKEN env vars")
(System/exit 1)))
(rotate-deploy-keys (System/getenv "GITHUB_TOKEN") (System/getenv "CIRCLE_TOKEN")))
(comment
(rotate-deploy-keys (System/getenv "GITHUB_TOKEN") (System/getenv "CIRCLE_TOKEN") true))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment