├── .github ├── .gitignore ├── FUNDING.yaml └── workflows │ └── check-standard.yaml ├── LICENSE ├── inst ├── tinytest │ └── test_pubcheck.R └── cmdline │ └── pubcheck.R ├── tests └── tinytest.R ├── .gitignore ├── NEWS.md ├── NAMESPACE ├── R ├── pubcheck-package.R ├── check-ssh-key.R ├── utils.R ├── check-gh-org-members.R ├── key-check.R └── check-ghuser.R ├── .Rbuildignore ├── pubcheck.Rproj ├── man ├── check_ssh_pub_key.Rd ├── check_gh_user_keys.Rd ├── check_gh_following.Rd ├── pubcheck.Rd ├── check_gh_repo_contributors.Rd └── check_gh_org_members.Rd ├── DESCRIPTION ├── LICENSE.md ├── CONDUCT.md ├── README.Rmd └── README.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: hrbrmstr 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: pubcheck authors 3 | -------------------------------------------------------------------------------- /inst/tinytest/test_pubcheck.R: -------------------------------------------------------------------------------- 1 | 2 | # Placeholder with simple test 3 | expect_equal(1 + 1, 2) 4 | 5 | -------------------------------------------------------------------------------- /tests/tinytest.R: -------------------------------------------------------------------------------- 1 | 2 | if ( requireNamespace("tinytest", quietly=TRUE) ){ 3 | tinytest::test_package("pubcheck") 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Rproj.user 3 | .Rhistory 4 | .RData 5 | .Rproj 6 | README_cache 7 | src/*.o 8 | src/*.so 9 | src/*.dll 10 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | * added `check_gh_org_members()` to check keys of all members of a specified organization 3 | 4 | 0.1.0 5 | * Initial release 6 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(check_gh_following) 4 | export(check_gh_org_members) 5 | export(check_gh_repo_contributors) 6 | export(check_gh_user_keys) 7 | export(check_ssh_pub_key) 8 | importFrom(gh,gh) 9 | importFrom(openssl,read_pubkey) 10 | -------------------------------------------------------------------------------- /R/pubcheck-package.R: -------------------------------------------------------------------------------- 1 | #' Check Safety of SSH Public Keys 2 | #' 3 | #' SSH is great! Poorly-configured SSH keys are not. Tools are provided to assess the 4 | #' safety of SSH public keys in multiple contexts. 5 | #' 6 | #' @md 7 | #' @name pubcheck 8 | #' @keywords internal 9 | #' @author Bob Rudis (bob@@rud.is) 10 | #' @importFrom gh gh 11 | #' @importFrom openssl read_pubkey 12 | "_PACKAGE" 13 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.vscode$ 2 | ^.*\.Rproj$ 3 | ^\.Rproj\.user$ 4 | ^\.travis\.yml$ 5 | ^README\.*Rmd$ 6 | ^README\.*html$ 7 | ^NOTES\.*Rmd$ 8 | ^NOTES\.*html$ 9 | ^\.codecov\.yml$ 10 | ^README_files$ 11 | ^doc$ 12 | ^docs$ 13 | ^tmp$ 14 | ^notes$ 15 | ^CONDUCT.*$ 16 | ^CODE.*$ 17 | ^\.gitlab-ci\.yml$ 18 | ^\.vscode$ 19 | ^CRAN-RELEASE$ 20 | ^tools$ 21 | ^LICENSE\.md$ 22 | ^bld$ 23 | ^node_modules^ 24 | ^package-lock\.json$ 25 | ^\.github$ 26 | -------------------------------------------------------------------------------- /pubcheck.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | StripTrailingWhitespace: Yes 16 | 17 | BuildType: Package 18 | PackageUseDevtools: Yes 19 | PackageInstallArgs: --no-multiarch --with-keep.source 20 | PackageBuildArgs: --resave-data 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /man/check_ssh_pub_key.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-ssh-key.R 3 | \name{check_ssh_pub_key} 4 | \alias{check_ssh_pub_key} 5 | \title{Check one SSH public key} 6 | \usage{ 7 | check_ssh_pub_key(keys) 8 | } 9 | \arguments{ 10 | \item{key}{Either a path to one or more files or literal public key strings} 11 | } 12 | \value{ 13 | data frame 14 | } 15 | \description{ 16 | Check one SSH public key 17 | } 18 | \examples{ 19 | \dontrun{ 20 | check_ssh_pub_key("~/.ssh/id_rsa.pub") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /man/check_gh_user_keys.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-ghuser.R 3 | \name{check_gh_user_keys} 4 | \alias{check_gh_user_keys} 5 | \title{Check one or more GitHub user's keys} 6 | \usage{ 7 | check_gh_user_keys(gh_users) 8 | } 9 | \arguments{ 10 | \item{gh_users}{(chr) a character vector of GitHub user ids} 11 | } 12 | \value{ 13 | data frame 14 | } 15 | \description{ 16 | Check one or more GitHub user's keys 17 | } 18 | \examples{ 19 | \dontrun{ 20 | check_gh_user_keys("hrbrmstr") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /R/check-ssh-key.R: -------------------------------------------------------------------------------- 1 | #' Check one SSH public key 2 | #' 3 | #' @param key Either a path to one or more files or literal public key strings 4 | #' @return data frame 5 | #' @export 6 | #' @examples \dontrun{ 7 | #' check_ssh_pub_key("~/.ssh/id_rsa.pub") 8 | #' } 9 | check_ssh_pub_key <- function(keys) { 10 | 11 | res <- lapply(keys, parse_key) 12 | 13 | data.frame( 14 | key = sapply(res, `[[`, "ssh"), 15 | algo = sapply(res, `[[`, "type"), 16 | len = sapply(res, `[[`, "size") 17 | ) -> keys 18 | 19 | apply(keys, 1, \(row) { 20 | key_check(row[["algo"]], row[["len"]]) 21 | }) -> keys$status 22 | 23 | keys 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /man/check_gh_following.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-ghuser.R 3 | \name{check_gh_following} 4 | \alias{check_gh_following} 5 | \title{Check all SSH keys of GitHub users a particular account is following} 6 | \usage{ 7 | check_gh_following(gh_user) 8 | } 9 | \arguments{ 10 | \item{gh_user}{a single GitHub user id} 11 | } 12 | \value{ 13 | data frame 14 | } 15 | \description{ 16 | Check all SSH keys of GitHub users a particular account is following 17 | } 18 | \note{ 19 | this may take a while for accounts that follow many users 20 | } 21 | \examples{ 22 | \dontrun{ 23 | check_gh_following("koenrh") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | possibly <- function(.f, otherwise, quiet = TRUE) { 2 | force(otherwise) 3 | function(...) { 4 | tryCatch( 5 | .f(...), 6 | error = function(e) { 7 | if (!quiet) 8 | message("Error: ", e$message) 9 | otherwise 10 | }, 11 | interrupt = function(e) { 12 | stop("Terminated by user", call. = FALSE) 13 | } 14 | ) 15 | } 16 | } 17 | 18 | p_gh_next <- possibly(gh::gh_next, list()) 19 | 20 | parse_key <- function(key) { 21 | if (is.na(key)) { 22 | list( 23 | type = NA_character_, 24 | size = NA_integer_ 25 | ) 26 | } else { 27 | openssl::read_pubkey(key) 28 | } 29 | } -------------------------------------------------------------------------------- /man/pubcheck.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/pubcheck-package.R 3 | \docType{package} 4 | \name{pubcheck} 5 | \alias{pubcheck} 6 | \alias{pubcheck-package} 7 | \title{Check Safety of SSH Public Keys} 8 | \description{ 9 | SSH is great! Poorly-configured SSH keys are not. Tools are provided to assess the 10 | safety of SSH public keys in multiple contexts. 11 | } 12 | \seealso{ 13 | Useful links: 14 | \itemize{ 15 | \item \url{https://github.com/hrbrmstr/pubcheck} 16 | \item Report bugs at \url{https://github.com/hrbrmstr/pubcheck/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | Bob Rudis (bob@rud.is) 22 | } 23 | \keyword{internal} 24 | -------------------------------------------------------------------------------- /man/check_gh_repo_contributors.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-ghuser.R 3 | \name{check_gh_repo_contributors} 4 | \alias{check_gh_repo_contributors} 5 | \title{Check all SSH keys of GitHub users a particular account is following} 6 | \usage{ 7 | check_gh_repo_contributors(owner, repo) 8 | } 9 | \arguments{ 10 | \item{owner}{github owner name} 11 | 12 | \item{repo}{github repo} 13 | } 14 | \value{ 15 | data frame 16 | } 17 | \description{ 18 | Check all SSH keys of GitHub users a particular account is following 19 | } 20 | \note{ 21 | this may take a while for repos with a large contributor list; this will also ignore "bots" 22 | } 23 | \examples{ 24 | \dontrun{ 25 | check_gh_repo_contributors("koenrh") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /man/check_gh_org_members.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-gh-org-members.R 3 | \name{check_gh_org_members} 4 | \alias{check_gh_org_members} 5 | \title{Check all SSH keys of GitHub users that are members of a specified GitHub organization} 6 | \usage{ 7 | check_gh_org_members(gh_org) 8 | } 9 | \arguments{ 10 | \item{gh_org}{a single GitHub organization name (e.g. "\code{GreyNoise-Intelligence}")} 11 | } 12 | \value{ 13 | data frame 14 | } 15 | \description{ 16 | Check all SSH keys of GitHub users that are members of a specified GitHub organization 17 | } 18 | \note{ 19 | this may take a while for organizations with many users 20 | } 21 | \examples{ 22 | \dontrun{ 23 | check_gh_org_members("GreyNoise-Intelligence") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: pubcheck 2 | Type: Package 3 | Title: Check Safety of SSH Public Keys 4 | Version: 0.4.0 5 | Date: 2022-10-08 6 | Authors@R: c( 7 | person("Bob", "Rudis", email = "bob@rud.is", role = c("aut", "cre"), 8 | comment = c(ORCID = "0000-0001-5670-2640")) 9 | ) 10 | Maintainer: Bob Rudis 11 | Description: SSH is great! Poorly-configured SSH keys are not. Tools are provided to assess the 12 | safety of SSH public keys in multiple contexts. 13 | URL: https://github.com/hrbrmstr/pubcheck 14 | BugReports: https://github.com/hrbrmstr/pubcheck/issues 15 | Encoding: UTF-8 16 | License: MIT + file LICENSE 17 | Suggests: 18 | tinytest, 19 | argparser, 20 | jsonlite, 21 | pander 22 | Depends: 23 | R (>= 4.1.0) 24 | Imports: 25 | gt, 26 | openssl 27 | Roxygen: list(markdown = TRUE) 28 | RoxygenNote: 7.2.1 29 | -------------------------------------------------------------------------------- /R/check-gh-org-members.R: -------------------------------------------------------------------------------- 1 | #' Check all SSH keys of GitHub users that are members of a specified GitHub organization 2 | #' 3 | #' @param gh_org a single GitHub organization name (e.g. "`GreyNoise-Intelligence`") 4 | #' @return data frame 5 | #' @note this may take a while for organizations with many users 6 | #' @export 7 | #' @examples \dontrun{ 8 | #' check_gh_org_members("GreyNoise-Intelligence") 9 | #' } 10 | check_gh_org_members <- function(gh_org) { 11 | 12 | stopifnot( 13 | `Can only specify one GitHub Organization to check members of` = 14 | (length(gh_org) == 1) 15 | ) 16 | 17 | res <- members <- gh::gh(sprintf("/orgs/%s/members", gh_org)) 18 | 19 | while (length(res) > 0) { 20 | res <- p_gh_next(res) 21 | members <- c(members, res) 22 | } 23 | 24 | res <- lapply(sapply(members, `[[`, "login"), check_gh_user_keys) 25 | 26 | do.call(rbind.data.frame, res) 27 | 28 | } -------------------------------------------------------------------------------- /R/key-check.R: -------------------------------------------------------------------------------- 1 | key_check <- \(algo, len) { 2 | 3 | len <- as.integer(trimws(len)) 4 | 5 | if (is.na(algo) || is.na(len)) return(NA_character_) 6 | 7 | if (algo == "dsa") { 8 | "❌ Key needs to be replaced; The DSA algorithm should not be used" 9 | } else if (algo == "rsa") { 10 | if (len >= 4096) { 11 | "✅ Key is safe" 12 | } else if (len >= 2048) { 13 | "✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096" 14 | } else { 15 | "❌ Key needs to be replaced; For the RSA algorithm at least 2048, recommended 4096" 16 | } 17 | } else if (algo == "ecdsa") { 18 | if (len == 521) { 19 | "✅ Key is safe" 20 | } else { 21 | "❌ For the ECDSA algorithm, it should be 521" 22 | } 23 | } else if (algo == "ed25519") { 24 | if (len >= 256) { 25 | "✅ Key is safe" 26 | } else { 27 | "❌ For the ED25519, the key size should be 256 or larger" 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 pubcheck authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/check-standard.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/master/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macOS-latest, r: 'release'} 22 | - {os: windows-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 24 | - {os: ubuntu-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'oldrel-1'} 26 | 27 | env: 28 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 29 | R_KEEP_PKG_SOURCE: yes 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | 34 | - uses: r-lib/actions/setup-pandoc@v1 35 | 36 | - uses: r-lib/actions/setup-r@v1 37 | with: 38 | r-version: ${{ matrix.config.r }} 39 | http-user-agent: ${{ matrix.config.http-user-agent }} 40 | use-public-rspm: true 41 | 42 | - uses: r-lib/actions/setup-r-dependencies@v1 43 | with: 44 | extra-packages: rcmdcheck 45 | 46 | - uses: r-lib/actions/check-r-package@v1 47 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who 4 | contribute through reporting issues, posting feature requests, updating documentation, 5 | submitting pull requests or patches, and other activities. 6 | 7 | We are committed to making participation in this project a harassment-free experience for 8 | everyone, regardless of level of experience, gender, gender identity and expression, 9 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 10 | 11 | Examples of unacceptable behavior by participants include the use of sexual language or 12 | imagery, derogatory comments or personal attacks, trolling, public or private harassment, 13 | insults, or other unprofessional conduct. 14 | 15 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 16 | commits, code, wiki edits, issues, and other contributions that are not aligned to this 17 | Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed 18 | from the project team. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 21 | opening an issue or contacting one or more of the project maintainers. 22 | 23 | This Code of Conduct is adapted from the Contributor Covenant 24 | (http:contributor-covenant.org), version 1.0.0, available at 25 | http://contributor-covenant.org/version/1/0/0/ 26 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: rmarkdown::github_document 3 | editor_options: 4 | chunk_output_type: console 5 | --- 6 | ```{r pkg-knitr-opts, include=FALSE} 7 | hrbrpkghelpr::global_opts() 8 | ``` 9 | 10 | ```{r badges, results='asis', echo=FALSE, cache=FALSE} 11 | hrbrpkghelpr::stinking_badges() 12 | ``` 13 | 14 | ```{r description, results='asis', echo=FALSE, cache=FALSE} 15 | hrbrpkghelpr::yank_title_and_description() 16 | ``` 17 | 18 | ## What's Inside The Tin 19 | 20 | The following functions are implemented: 21 | 22 | ```{r ingredients, results='asis', echo=FALSE, cache=FALSE} 23 | hrbrpkghelpr::describe_ingredients() 24 | ``` 25 | 26 | ## Installation 27 | 28 | ```{r install-ex, results='asis', echo=FALSE, cache=FALSE} 29 | hrbrpkghelpr::install_block() 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```{r lib-ex} 35 | library(pubcheck) 36 | library(tidyverse) 37 | 38 | # current version 39 | packageVersion("pubcheck") 40 | 41 | ``` 42 | 43 | ### Local file 44 | 45 | ```{r ex-01} 46 | check_ssh_pub_key("~/.ssh/id_rsa.pub") |> 47 | mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 48 | knitr::kable() 49 | ``` 50 | 51 | ### A GitHub user 52 | 53 | ```{r ex-02} 54 | check_gh_user_keys(c("hrbrmstr", "mikemahoney218")) |> 55 | mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 56 | knitr::kable() 57 | ``` 58 | 59 | ### Keys of all the users a GitHub account is following 60 | 61 | ```{r ex-03} 62 | check_gh_following("koenrh") |> 63 | mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 64 | knitr::kable() 65 | ``` 66 | 67 | ```{r ex-04} 68 | check_gh_repo_contributors("hrbrmstr", "ggalt") |> 69 | mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 70 | knitr::kable() 71 | ``` 72 | 73 | ## pubcheck Metrics 74 | 75 | ```{r cloc, echo=FALSE} 76 | cloc::cloc_pkg_md() 77 | ``` 78 | 79 | ## Code of Conduct 80 | 81 | Please note that this project is released with a Contributor Code of Conduct. 82 | By participating in this project you agree to abide by its terms. 83 | -------------------------------------------------------------------------------- /inst/cmdline/pubcheck.R: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/Rscript --vanilla 2 | 3 | suppressPackageStartupMessages({ 4 | library(pubcheck, quietly = TRUE) 5 | library(argparser, quietly = TRUE) 6 | library(pander, quietly = TRUE) 7 | library(digest, quietly = TRUE) 8 | library(jsonlite, include.only = "stream_out", quietly = TRUE) 9 | }) 10 | 11 | arg_parser( 12 | description = "Check safety configuration of SSH public keys", 13 | hide.opts = TRUE 14 | ) -> p 15 | 16 | add_argument( 17 | parser = p, 18 | arg = "--format", 19 | flag = FALSE, 20 | default = "json", 21 | help = "Results output format. One of `json` or `table`. Note that the key itself will not be displayed if `table` is chosen." 22 | ) -> p 23 | 24 | add_argument( 25 | parser = p, 26 | arg = "--file", 27 | flag = FALSE, 28 | help = "Check local ssh public key file." 29 | ) -> p 30 | 31 | add_argument( 32 | parser = p, 33 | arg = "--user", 34 | flag = FALSE, 35 | help = "Check SSH keys for a specified GitHub user. NOTE: if using this option with `table` as the format, the MD5 value of the keys will be used to make the output more readable." 36 | ) -> p 37 | 38 | argv <- parse_args(p) 39 | 40 | if (utils::hasName(argv, "help") && argv$help) { 41 | print(p) 42 | quit(save = "no", status = 0, runLast = FALSE) 43 | } 44 | 45 | if (utils::hasName(argv, "file") && (!is.na(argv$file))) { 46 | 47 | res <- check_ssh_pub_key(path.expand(argv$file)) 48 | res$file <- argv$file 49 | 50 | if (argv$format == "json") { 51 | jsonlite::stream_out(res, verbose = FALSE) 52 | quit(save = "no", status = 0, runLast = FALSE) 53 | } else if (argv$format == "table") { 54 | res <- res[, c("file", "algo", "len", "status")] 55 | pander::pandoc.table(res, style = 'simple') 56 | quit(save = "no", status = 0, runLast = FALSE) 57 | } else { 58 | message("Unknown format: ", p$format) 59 | quit(save = "no", status = 1, runLast = FALSE) 60 | } 61 | 62 | } else if (utils::hasName(argv, "user")) { 63 | 64 | res <- check_gh_user_keys(argv$user) 65 | # res$file <- argv$file 66 | if (argv$format == "json") { 67 | jsonlite::stream_out(res, verbose = FALSE) 68 | quit(save = "no", status = 0, runLast = FALSE) 69 | } else if (argv$format == "table") { 70 | res$md5 <- sapply(res$key, digest::digest, "md5", USE.NAMES = FALSE) 71 | res$key <- NULL 72 | pander::pandoc.table(res, style = 'simple') 73 | quit(save = "no", status = 0, runLast = FALSE) 74 | } else { 75 | message("Unknown format: ", p$format) 76 | quit(save = "no", status = 1, runLast = FALSE) 77 | } 78 | 79 | } 80 | 81 | print(p) 82 | 83 | -------------------------------------------------------------------------------- /R/check-ghuser.R: -------------------------------------------------------------------------------- 1 | .check_one_gh_user <- function(gh_user) { 2 | 3 | keys <- readLines(sprintf("https://github.com/%s.keys", gh_user)) 4 | 5 | if (length(keys) == 0) keys <- NA_character_ 6 | 7 | data.frame( 8 | user = gh_user, 9 | key = keys 10 | ) -> user 11 | 12 | res <- lapply(user$key, parse_key) 13 | 14 | user$algo <- sapply(res, `[[`, "type") 15 | user$len <- sapply(res, `[[`, "size") 16 | 17 | apply(user, 1, \(row) { 18 | key_check(row[["algo"]], row[["len"]]) 19 | }) -> user$status 20 | 21 | user 22 | 23 | } 24 | 25 | #' Check one or more GitHub user's keys 26 | #' 27 | #' @param gh_users (chr) a character vector of GitHub user ids 28 | #' @return data frame 29 | #' @export 30 | #' @examples \dontrun{ 31 | #' check_gh_user_keys("hrbrmstr") 32 | #' } 33 | check_gh_user_keys <- function(gh_users) { 34 | do.call(rbind.data.frame, lapply(gh_users, .check_one_gh_user)) 35 | } 36 | 37 | #' Check all SSH keys of GitHub users a particular account is following 38 | #' 39 | #' @param gh_user a single GitHub user id 40 | #' @return data frame 41 | #' @note this may take a while for accounts that follow many users 42 | #' @export 43 | #' @examples \dontrun{ 44 | #' check_gh_following("koenrh") 45 | #' } 46 | check_gh_following <- function(gh_user) { 47 | 48 | stopifnot(`Can only specify one GitHub user to check followers of`=(length(gh_user)==1)) 49 | 50 | res <- following <- gh::gh(sprintf("/users/%s/following", gh_user)) 51 | while (length(res) > 0) { 52 | res <- p_gh_next(res) 53 | following <- c(following, res) 54 | } 55 | 56 | sapply(following, `[[`, "login") |> 57 | lapply(check_gh_user_keys) -> res 58 | 59 | do.call(rbind.data.frame, res) 60 | 61 | } 62 | 63 | #' Check all SSH keys of GitHub users a particular account is following 64 | #' 65 | #' @param owner github owner name 66 | #' @param repo github repo 67 | #' @return data frame 68 | #' @note this may take a while for repos with a large contributor list; this will also ignore "bots" 69 | #' @export 70 | #' @examples \dontrun{ 71 | #' check_gh_repo_contributors("koenrh") 72 | #' } 73 | check_gh_repo_contributors <- function(owner, repo) { 74 | 75 | stopifnot(`Can only specify one GitHub repo to check followers of`=(length(owner)==1)) 76 | stopifnot(`Can only specify one GitHub repo to check followers of`=(length(repo)==1)) 77 | 78 | res <- contributing <- gh::gh(sprintf("/repos/%s/%s/contributors", owner, repo)) 79 | while (length(res) > 0) { 80 | res <- p_gh_next(res) 81 | contributing <- c(contributing, res) 82 | } 83 | 84 | grep("[[:alnum:]-]", sapply(contributing, `[[`, "login"), value=TRUE) |> 85 | lapply(check_gh_user_keys) -> res 86 | 87 | do.call(rbind.data.frame, res) 88 | 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Project Status: Active – The project has reached a stable, usable 3 | state and is being actively 4 | developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 5 | [![Signed 6 | by](https://img.shields.io/badge/Keybase-Verified-brightgreen.svg)](https://keybase.io/hrbrmstr) 7 | ![Signed commit 8 | %](https://img.shields.io/badge/Signed_Commits-0%25-lightgrey.svg) 9 | [![R-CMD-check](https://github.com/hrbrmstr/pubcheck/workflows/R-CMD-check/badge.svg)](https://github.com/hrbrmstr/pubcheck/actions?query=workflow%3AR-CMD-check) 10 | [![Linux build 11 | Status](https://travis-ci.org/hrbrmstr/pubcheck.svg?branch=master)](https://travis-ci.org/hrbrmstr/pubcheck) 12 | [![Coverage 13 | Status](https://codecov.io/gh/hrbrmstr/pubcheck/branch/master/graph/badge.svg)](https://codecov.io/gh/hrbrmstr/pubcheck) 14 | ![Minimal R 15 | Version](https://img.shields.io/badge/R%3E%3D-4.1.0-blue.svg) 16 | ![License](https://img.shields.io/badge/License-MIT-blue.svg) 17 | 18 | # pubcheck 19 | 20 | Check Safety of SSH Public Keys 21 | 22 | ## Description 23 | 24 | SSH is great! Poorly-configured SSH keys are not. Tools are provided to 25 | assess the safety of SSH public keys in multiple contexts. 26 | 27 | ## What’s Inside The Tin 28 | 29 | The following functions are implemented: 30 | 31 | - `check_gh_following`: Check all SSH keys of GitHub users a 32 | particular account is following 33 | - `check_gh_org_members`: Check all SSH keys of GitHub users that are 34 | members of a specified GitHub organization 35 | - `check_gh_repo_contributors`: Check all SSH keys of GitHub users a 36 | particular account is following 37 | - `check_gh_user_keys`: Check one or more GitHub user’s keys 38 | - `check_ssh_pub_key`: Check one SSH public key 39 | 40 | ## Installation 41 | 42 | ``` r 43 | remotes::install_github("hrbrmstr/pubcheck") 44 | ``` 45 | 46 | NOTE: To use the ‘remotes’ install options you will need to have the 47 | [{remotes} package](https://github.com/r-lib/remotes) installed. 48 | 49 | ## Usage 50 | 51 | ``` r 52 | library(pubcheck) 53 | library(tidyverse) 54 | 55 | # current version 56 | packageVersion("pubcheck") 57 | ## [1] '0.3.0' 58 | ``` 59 | 60 | ### Local file 61 | 62 | ``` r 63 | check_ssh_pub_key("~/.ssh/id_rsa.pub") |> 64 | mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 65 | knitr::kable() 66 | ``` 67 | 68 | | key | algo | len | status | 69 | |:--------------------------------|:-----|-----:|:---------------| 70 | | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 4096 | ✅ Key is safe | 71 | 72 | ### A GitHub user 73 | 74 | ``` r 75 | check_gh_user_keys(c("hrbrmstr", "mikemahoney218")) |> 76 | mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 77 | knitr::kable() 78 | ``` 79 | 80 | | user | key | algo | len | status | 81 | |:---------------|:--------------------------------|:--------|-----:|:----------------------------------------------------------------------| 82 | | hrbrmstr | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 83 | | hrbrmstr | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 84 | | hrbrmstr | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 4096 | ✅ Key is safe | 85 | | mikemahoney218 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 4096 | ✅ Key is safe | 86 | | mikemahoney218 | ssh-ed25519 AAAAC3NzaC1lZDI1NT… | ed25519 | 256 | ✅ Key is safe | 87 | | mikemahoney218 | ssh-ed25519 AAAAC3NzaC1lZDI1NT… | ed25519 | 256 | ✅ Key is safe | 88 | | mikemahoney218 | ssh-ed25519 AAAAC3NzaC1lZDI1NT… | ed25519 | 256 | ✅ Key is safe | 89 | 90 | ### Keys of all the users a GitHub account is following 91 | 92 | ``` r 93 | check_gh_following("koenrh") |> 94 | mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 95 | knitr::kable() 96 | ``` 97 | 98 | | user | key | algo | len | status | 99 | |:-------|:--------------------------------|:-----|-----:|:----------------------------------------------------------------------| 100 | | framer | NA | NA | NA | NA | 101 | | jurre | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 102 | 103 | ``` r 104 | check_gh_repo_contributors("hrbrmstr", "ggalt") |> 105 | mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 106 | knitr::kable() 107 | ``` 108 | 109 | | user | key | algo | len | status | 110 | |:------------|:--------------------------------|:--------|-----:|:----------------------------------------------------------------------| 111 | | hrbrmstr | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 112 | | hrbrmstr | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 113 | | hrbrmstr | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 4096 | ✅ Key is safe | 114 | | hcraT | NA | NA | NA | NA | 115 | | yonicd | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 3072 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 116 | | yonicd | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 117 | | yonicd | ssh-ed25519 AAAAC3NzaC1lZDI1NT… | ed25519 | 256 | ✅ Key is safe | 118 | | yonicd | ssh-ed25519 AAAAC3NzaC1lZDI1NT… | ed25519 | 256 | ✅ Key is safe | 119 | | bbolker | ssh-rsa AAAAB3NzaC1yc2EAAAABIw… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 120 | | benmarwick | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 121 | | cpsievert | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 122 | | jankatins | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 123 | | jankatins | ssh-ed25519 AAAAC3NzaC1lZDI1NT… | ed25519 | 256 | ✅ Key is safe | 124 | | jonocarroll | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 4096 | ✅ Key is safe | 125 | | jonocarroll | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 2048 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 126 | | jonocarroll | ssh-ed25519 AAAAC3NzaC1lZDI1NT… | ed25519 | 256 | ✅ Key is safe | 127 | | pkq | NA | NA | NA | NA | 128 | | rplzzz | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 3072 | ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096 | 129 | | jjchern | NA | NA | NA | NA | 130 | | larmarange | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 4096 | ✅ Key is safe | 131 | | larmarange | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… | rsa | 4096 | ✅ Key is safe | 132 | 133 | ## pubcheck Metrics 134 | 135 | | Lang | \# Files | (%) | LoC | (%) | Blank lines | (%) | \# Lines | (%) | 136 | |:-----|---------:|-----:|----:|-----:|------------:|-----:|---------:|-----:| 137 | | R | 7 | 0.35 | 124 | 0.34 | 41 | 0.28 | 55 | 0.29 | 138 | | YAML | 2 | 0.10 | 35 | 0.10 | 10 | 0.07 | 2 | 0.01 | 139 | | Rmd | 1 | 0.05 | 21 | 0.06 | 22 | 0.15 | 39 | 0.20 | 140 | | SUM | 10 | 0.50 | 180 | 0.50 | 73 | 0.50 | 96 | 0.50 | 141 | 142 | clock Package Metrics for pubcheck 143 | 144 | ## Code of Conduct 145 | 146 | Please note that this project is released with a Contributor Code of 147 | Conduct. By participating in this project you agree to abide by its 148 | terms. 149 | --------------------------------------------------------------------------------