├── .Rbuildignore ├── .github ├── CONTRIBUTING.md ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .travis.yml ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── addins.R ├── check_namespace.R ├── defender-package.R ├── system_calls.R └── utils.R ├── README-NOT.md ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── codecov.yml ├── codemeta.json ├── defender.Rproj ├── inst └── rstudio │ └── addins.dcf ├── man ├── check_namespace.Rd ├── dangerous_imports.Rd ├── defender-package.Rd ├── figures │ ├── logo.png │ └── supergb.png ├── summarize_system_calls.Rd └── system_calls.Rd └── tests ├── testthat.R └── testthat ├── test-check_namespace.R ├── test-system_calls.R └── test-utils.R /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^\.github$ 2 | ^.*\.Rproj$ 3 | ^\.Rproj\.user$ 4 | ^\.travis\.yml$ 5 | ^codecov\.yml$ 6 | ^LICENSE\.md$ 7 | ^README\.Rmd$ 8 | ^README-.*\.png$ 9 | ^_pkgdown\.yml$ 10 | ^docs$ 11 | ^codemeta\.json$ 12 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING # 2 | 3 | ### Fixing typos 4 | 5 | Small typos or grammatical errors in documentation may be edited directly using 6 | the GitHub web interface, so long as the changes are made in the _source_ file. 7 | 8 | * YES: you edit a roxygen comment in a `.R` file below `R/`. 9 | * NO: you edit an `.Rd` file below `man/`. 10 | 11 | ### Prerequisites 12 | 13 | Before you make a substantial pull request, you should always file an issue and 14 | make sure someone from the team agrees that it’s a problem. If you’ve found a 15 | bug, create an associated issue and illustrate the bug with a minimal 16 | [reprex](https://www.tidyverse.org/help/#reprex). 17 | 18 | ### Pull request process 19 | 20 | * We recommend that you create a Git branch for each pull request (PR). 21 | * Look at the Travis and AppVeyor build status before and after making changes. 22 | The `README` should contain badges for any continuous integration services used 23 | by the package. 24 | * We recommend the tidyverse [style guide](http://style.tidyverse.org). 25 | You can use the [styler](https://CRAN.R-project.org/package=styler) package to 26 | apply these styles, but please don't restyle code that has nothing to do with 27 | your PR. 28 | * We use [roxygen2](https://cran.r-project.org/package=roxygen2). 29 | * We use [testthat](https://cran.r-project.org/package=testthat). Contributions 30 | with test cases included are easier to accept. 31 | * For user-facing changes, add a bullet to the top of `NEWS.md` below the 32 | current development version header describing the changes made followed by your 33 | GitHub username, and links to relevant issue(s)/PR(s). 34 | 35 | ### Code of Conduct 36 | 37 | Please note that the defender project is released with a 38 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this 39 | project you agree to abide by its terms. 40 | 41 | ### See rOpenSci [contributing guide](https://ropensci.github.io/dev_guide/contributingguide.html) 42 | for further details. 43 | 44 | ### Discussion forum 45 | 46 | Check out our [discussion forum](https://discuss.ropensci.org) if 47 | 48 | * you have a question, an use case, or otherwise not a bug or feature request for the software itself. 49 | * you think your issue requires a longer form discussion. 50 | 51 | ### Prefer to Email? 52 | 53 | Email the person listed as maintainer in the `DESCRIPTION` file of this repo. 54 | 55 | Though note that private discussions over email don't help others - of course email is totally warranted if it's a sensitive problem of any kind. 56 | 57 | ### Thanks for contributing! 58 | 59 | This contributing guide is adapted from the tidyverse contributing guide available at https://raw.githubusercontent.com/r-lib/usethis/master/inst/templates/tidy-contributing.md 60 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Session Info 6 | 7 | ```r 8 | 9 | ``` 10 |
11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ## Description 8 | 9 | 10 | ## Related Issue 11 | 14 | 15 | ## Example 16 | 18 | 19 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | 3 | language: R 4 | sudo: false 5 | cache: packages 6 | 7 | after_success: 8 | - Rscript -e 'covr::codecov()' 9 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: defender 2 | Title: Check Package for Potential Security Violations 3 | Version: 0.0.1.9000 4 | Authors@R: c( 5 | person("Ildiko", "Czeller", email = "czeildi@gmail.com", role = c("aut", "cre")), 6 | person("Karthik", "Ram", email = "karthik.ram@gmail.com", role = c("aut"), 7 | comment = c(ORCID = " 0000-0002-0233-1757")), 8 | person("Bob", "Rudis", email = "bob@rud.is", role = c("aut"), 9 | comment = c(ORCID = "0000-0001-5670-2640")), 10 | person("Kara", "Woo", role = "aut") 11 | ) 12 | Maintainer: ropensci 13 | Description: Check an R package for potential security risks and violations 14 | via static code analysis. 15 | Depends: R (>= 3.1.0) 16 | License: MIT + file LICENSE 17 | Encoding: UTF-8 18 | LazyData: true 19 | Suggests: 20 | covr, 21 | testthat, 22 | knitr, 23 | roxygen2, 24 | rstudioapi, 25 | shiny, 26 | miniUI 27 | RoxygenNote: 6.1.1 28 | Roxygen: list(markdown = TRUE) 29 | Imports: 30 | magrittr, 31 | utils, 32 | withr 33 | URL: https://docs.ropensci.org/defender/, https://github.com/ropenscilabs/defender 34 | BugReports: https://github.com/ropenscilabs/defender/issues 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2018 2 | COPYRIGHT HOLDER: Ildi Czeller 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2018 Ildi Czeller 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 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(check_namespace) 4 | export(dangerous_imports) 5 | export(summarize_system_calls) 6 | export(system_calls) 7 | importFrom(magrittr,"%>%") 8 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # defender 0.0.0.9000 2 | 3 | * Added a `NEWS.md` file to track changes to the package. 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /R/addins.R: -------------------------------------------------------------------------------- 1 | system_calls_addin <- function() { 2 | ui <- miniUI::miniPage( 3 | miniUI::gadgetTitleBar("List system calls"), 4 | miniUI::miniContentPanel( 5 | shiny::textInput("evil_path", "path:", value = ".") 6 | ) 7 | ) 8 | 9 | server <- function(input, output, session) { 10 | shiny::observeEvent(input$done, { 11 | rstudioapi::sendToConsole( 12 | paste0("defender::summarize_system_calls('", input$evil_path, "')") 13 | ) 14 | shiny::stopApp() 15 | }) 16 | } 17 | 18 | viewer <- shiny::dialogViewer("system calls") 19 | shiny::runGadget(ui, server, viewer = viewer) 20 | } 21 | 22 | check_namespace_addin <- function() { 23 | ui <- miniUI::miniPage( 24 | miniUI::gadgetTitleBar("List system imports"), 25 | miniUI::miniContentPanel( 26 | shiny::textInput("evil_path", "path:", value = ".") 27 | ) 28 | ) 29 | 30 | server <- function(input, output, session) { 31 | shiny::observeEvent(input$done, { 32 | rstudioapi::sendToConsole( 33 | paste0("defender::check_namespace('", input$evil_path, "')") 34 | ) 35 | shiny::stopApp() 36 | }) 37 | } 38 | 39 | viewer <- shiny::dialogViewer("list system imports") 40 | shiny::runGadget(ui, server, viewer = viewer) 41 | } 42 | -------------------------------------------------------------------------------- /R/check_namespace.R: -------------------------------------------------------------------------------- 1 | #' Read a package NAMESPACE and check for dangerous imports 2 | #' 3 | #' Given a path to a package source tree, return a data.frame of Imports (both whole 4 | #' packages and fully qualified references). 5 | #' 6 | #' @md 7 | #' @param pkg_path path to package source tree 8 | #' @param imports_to_flag character vector of dangerous items to find 9 | #' @export 10 | #' @examples \dontrun{ 11 | #' check_namespace("../testevil") 12 | #' check_namespace( 13 | #' "../testevil", 14 | #' dangerous_imports(additional_dangerous_imports = "sys::exec_background") 15 | #' ) 16 | #' } 17 | check_namespace <- function(pkg_path, imports_to_flag = dangerous_imports()) { 18 | assert_path_exists(pkg_path) 19 | assert_is_package(pkg_path) 20 | 21 | imports_list <- parse_ns_file(pkg_path)$imports 22 | 23 | parsed_imports <- parse_all_imports(imports_list) 24 | all_imports <- summarize_imports( 25 | parsed_imports[["imported_packages"]], parsed_imports[["imported_functions"]] 26 | ) 27 | 28 | all_imports %>% 29 | subset(.$import %in% imports_to_flag) %>% 30 | `row.names<-`(NULL) 31 | } 32 | 33 | parse_all_imports <- function(imports_list) { 34 | whole_pkg_imports <- extract_whole_pkg_imports(imports_list) 35 | fqrs <- extract_fully_qualified_references(imports_list) 36 | 37 | imported_packages <- c( 38 | whole_pkg_imports, 39 | extract_pkgs_from_fully_qualified_references(fqrs) 40 | ) %>% 41 | unique() 42 | imported_functions <- transform_fully_qualified_refereces(fqrs) 43 | 44 | list( 45 | "imported_packages" = imported_packages, 46 | "imported_functions" = imported_functions 47 | ) 48 | } 49 | 50 | summarize_imports <- function(imported_packages, imported_functions) { 51 | if (length(imported_packages) > 0) { 52 | pkgs <- data.frame(type = "package", import = as.character(imported_packages), stringsAsFactors = FALSE) 53 | } else { 54 | pkgs <- data.frame(type = character(0), import = character(0)) 55 | } 56 | 57 | if (length(imported_functions) > 0) { 58 | funs <- data.frame(type = "function", import = as.character(imported_functions), stringsAsFactors = FALSE) 59 | } else { 60 | funs <- data.frame(type = character(0), import = character(0)) 61 | } 62 | 63 | all_imports <- rbind(pkgs, funs) 64 | if (nrow(all_imports) > 0) { 65 | all_imports$package <- vapply( 66 | strsplit(all_imports$import, "::"), 67 | function(x) x[[1]], 68 | "character" 69 | ) 70 | } else { 71 | all_imports$package <- character(0) 72 | } 73 | all_imports 74 | } 75 | 76 | parse_ns_file <- function(pkg_path) { 77 | parseNamespaceFile( 78 | basename(pkg_path), dirname(pkg_path), 79 | mustExist = FALSE 80 | ) 81 | } 82 | 83 | extract_whole_pkg_imports <- function(imports) { 84 | unlist(imports[lengths(imports) == 1], use.names = FALSE) 85 | } 86 | 87 | extract_fully_qualified_references <- function(imports) { 88 | imports[(lengths(imports) == 2)] 89 | } 90 | 91 | extract_pkgs_from_fully_qualified_references <- function(fqrs) { 92 | sapply(fqrs, function(x) x[[1]]) 93 | } 94 | 95 | transform_fully_qualified_refereces <- function(fqrs) { 96 | sapply(fqrs, function(x) { 97 | sprintf("%s::%s", x[[1]], x[[2]]) 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /R/defender-package.R: -------------------------------------------------------------------------------- 1 | utils::globalVariables(c("token", "text", ".")) 2 | 3 | #' @importFrom magrittr %>% 4 | #' @keywords internal 5 | "_PACKAGE" 6 | -------------------------------------------------------------------------------- /R/system_calls.R: -------------------------------------------------------------------------------- 1 | #' summarize occurrences of system calls in project 2 | #' 3 | #' Summarize and show occurrences of system calls in the R files in a given 4 | #' folder. There could be more occurrences. These calls should be further checked 5 | #' for maliciousness. 6 | #' 7 | #' @param path character, relative or absolute path for local code directory 8 | #' @param calls_to_flag character vector of functions names to flag as 9 | #' potentially dangerous system calls 10 | #' 11 | #' @return data.frame with a row for each system call 12 | #' @export 13 | #' 14 | #' @examples \dontrun{ 15 | #' # git clone git@github.com:ropenscilabs/testevil.git 16 | #' summarize_system_calls("testevil") 17 | #' summarize_system_calls("testevil", system_calls("exec_background")) 18 | #' } 19 | summarize_system_calls <- function(path = ".", calls_to_flag = system_calls()) { 20 | assert_path_exists(path) 21 | 22 | r_paths <- get_r_script_paths(path = path) 23 | summaries_by_file <- lapply(r_paths, digest_system_calls, path, calls_to_flag) 24 | Reduce(rbind, summaries_by_file) 25 | } 26 | 27 | digest_system_calls <- function(r_file, path, calls_to_flag = system_calls()) { 28 | result <- find_system_calls(parse(r_file), calls_to_flag) 29 | if (nrow(result) == 0L) { 30 | return(result) 31 | } 32 | result$path <- sub(paste0("^", path, "/"), "", r_file) 33 | subset(result, select = c("path", "line1", "text", "function_name")) %>% 34 | magrittr::set_names(c("path", "line_number", "call", "function_name")) 35 | } 36 | 37 | find_system_calls <- function(expr, calls_to_flag = system_calls()) { 38 | parsed_data <- withr::with_options( 39 | c("keep.source" = TRUE), 40 | utils::getParseData(expr, includeText = TRUE) 41 | ) 42 | 43 | base_fun_row_indexes <- which( 44 | (parsed_data$token == "SYMBOL_FUNCTION_CALL") & (parsed_data$text %in% calls_to_flag) 45 | ) 46 | base_sys_calls <- parsed_data[base_fun_row_indexes - 1, ] 47 | base_sys_calls$function_name <- parsed_data[base_fun_row_indexes, ]$text 48 | 49 | pkg_fun_row_indexes <- which( 50 | (parsed_data$token == "expr") & (parsed_data$text %in% calls_to_flag[grepl("::", calls_to_flag)]) 51 | ) 52 | pkg_sys_calls <- parsed_data[pkg_fun_row_indexes - 1, ] 53 | pkg_sys_calls$function_name <- parsed_data[pkg_fun_row_indexes, ]$text 54 | 55 | rbind(base_sys_calls, pkg_sys_calls) %>% 56 | `row.names<-`(NULL) 57 | } 58 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | #' Dangerous Imports in Namespace 2 | #' 3 | #' Character vector of all imports considered dangerous to be used with 4 | #' \code{\link{check_namespace}}. 5 | #' 6 | #' @param additional_dangerous_imports character vector of user-defined 7 | #' dangerous imports 8 | #' 9 | #' @return character vector of all imports considered dangerous 10 | #' @export 11 | #' 12 | #' @examples dangerous_imports() 13 | dangerous_imports <- function(additional_dangerous_imports = character(0)) { 14 | c(pkgs_doing_system_calls(), "processx::run", additional_dangerous_imports) %>% 15 | unique() 16 | } 17 | 18 | #' Functions making System Calls 19 | #' 20 | #' Character vector of functions making system calls to check for in R source 21 | #' files. 22 | #' 23 | #' @param additional_system_calls character vector of user-defined 24 | #' system calls 25 | #' 26 | #' @return character vector of all system calls 27 | #' @export 28 | #' 29 | #' @examples system_calls("exec_background") 30 | system_calls <- function(additional_system_calls = character(0)) { 31 | c( 32 | base_system_calls(), 33 | "processx::run", "sys::exec_internal", 34 | additional_system_calls 35 | ) %>% 36 | unique() 37 | } 38 | 39 | base_system_calls <- function() { 40 | c("system", "system2") 41 | } 42 | 43 | pkgs_doing_system_calls <- function() { 44 | c("sys", "processx") 45 | } 46 | 47 | get_r_script_paths <- function(path = ".") { 48 | list.files(path = path, pattern = "[rR]$", recursive = TRUE, full.names = TRUE) 49 | } 50 | 51 | assert_path_exists <- function(path) { 52 | if (!dir.exists(path)) { 53 | stop( 54 | paste0("Path ", path, " not found, you may want to clone the repository first."), 55 | call. = FALSE 56 | ) 57 | } 58 | } 59 | 60 | assert_is_package <- function(path) { 61 | if (!"DESCRIPTION" %in% list.files(path)) { 62 | stop(paste0(path, "is not a package directory"), call. = FALSE) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README-NOT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | rOpenSci Unconf 18 Project : defender 4 | ===================================== 5 | 6 | [![Project Status: Abandoned – Initial development has started, but there has not yet been a stable, usable release; the project has been abandoned and the author(s) do not intend on continuing development.](https://www.repostatus.org/badges/latest/abandoned.svg)](https://www.repostatus.org/#abandoned) 7 | 8 | 9 | 10 | 11 | defender 12 | ======================================================== 13 | 14 | 15 | [![Travis build status](https://travis-ci.org/ropenscilabs/defender.svg?branch=master)](https://travis-ci.org/ropenscilabs/defender) [![Coverage status](https://img.shields.io/codecov/c/github/ropenscilabs/defender/master.svg)](https://codecov.io/github/ropenscilabs/defender?branch=master) [![Lifecycle Status](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/) 16 | 17 | 18 | The goal of defender is to do static code analysis on other R packages to check for potential security risks and best practices. It provides checks on multiple levels: 19 | 20 | 1. \[x\] static code analysis without installing the package 21 | 2. \[ \] more thorough but potentially dangerous checks with installation / in Docker container 22 | 23 | The checks do not tell you whether something is harmful but rather they flag code that you should double-check before running / loading the package. 24 | 25 | Installation 26 | ------------ 27 | 28 | You can install defender from github with: 29 | 30 | ``` r 31 | # install.packages("devtools") 32 | devtools::install_github("ropenscilabs/defender") 33 | ``` 34 | 35 | Example 36 | ------- 37 | 38 | ### System calls in R scripts 39 | 40 | You can check for system calls in any directory locally available: 41 | 42 | ``` r 43 | defender::summarize_system_calls("../testevil") 44 | #> path line_number call 45 | #> 1 inst/root_sys.R 1 system2("ls") 46 | #> 2 inst/root_sys.R 4 system("ls") 47 | #> 3 R/exported.R 7 system2("ls") 48 | #> 4 R/internal.R 4 system("ls") 49 | #> 5 R/internal.R 8 system("ls -la") 50 | #> 6 R/processx.R 3 processx::run("ls") 51 | #> 7 R/sys.R 8 sys::exec_internal("ls") 52 | #> 8 R/system_hidden.R 2 system2("lm") 53 | #> function_name 54 | #> 1 system2 55 | #> 2 system 56 | #> 3 system2 57 | #> 4 system 58 | #> 5 system 59 | #> 6 processx::run 60 | #> 7 sys::exec_internal 61 | #> 8 system2 62 | ``` 63 | 64 | You can also include additional elements to flag as dangerous: 65 | 66 | ``` r 67 | sc <- defender::system_calls("poll") 68 | defender::summarize_system_calls("../testevil", calls_to_flag = sc) 69 | #> path line_number call 70 | #> 1 inst/root_sys.R 1 system2("ls") 71 | #> 2 inst/root_sys.R 4 system("ls") 72 | #> 3 R/exported.R 7 system2("ls") 73 | #> 4 R/internal.R 4 system("ls") 74 | #> 5 R/internal.R 8 system("ls -la") 75 | #> 6 R/processx.R 9 poll("ls") 76 | #> 7 R/processx.R 3 processx::run("ls") 77 | #> 8 R/sys.R 8 sys::exec_internal("ls") 78 | #> 9 R/system_hidden.R 2 system2("lm") 79 | #> function_name 80 | #> 1 system2 81 | #> 2 system 82 | #> 3 system2 83 | #> 4 system 84 | #> 5 system 85 | #> 6 poll 86 | #> 7 processx::run 87 | #> 8 sys::exec_internal 88 | #> 9 system2 89 | ``` 90 | 91 | ### System-related imports in NAMESPACE 92 | 93 | You can check the NAMESPACE file in a package for dangerous imports: 94 | 95 | ``` r 96 | defender::check_namespace("../testevil") 97 | #> type import package 98 | #> 1 package sys sys 99 | #> 2 package processx processx 100 | #> 3 function processx::run processx 101 | ``` 102 | 103 | You can also include additional elements to flag as dangerous: 104 | 105 | ``` r 106 | di <- defender::dangerous_imports("processx::poll") 107 | defender::check_namespace("../testevil", imports_to_flag = di) 108 | #> type import package 109 | #> 1 package sys sys 110 | #> 2 package processx processx 111 | #> 3 function processx::poll processx 112 | #> 4 function processx::run processx 113 | ``` 114 | 115 | Collaborators 116 | ------------- 117 | 118 | - Ildi Czeller @czeildi 119 | - Karthik Ram @karthik 120 | - Bob Rudis @hrbrmstr 121 | - Kara Woo @karawoo 122 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | # rOpenSci Unconf 18 Project : defender 7 | 8 | 9 | 10 | 11 | ```{r, echo = FALSE} 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>", 15 | fig.path = "README-" 16 | ) 17 | ``` 18 | 19 | # defender 20 | 21 | 22 | [![Travis build status](https://travis-ci.org/ropenscilabs/defender.svg?branch=master)](https://travis-ci.org/ropenscilabs/defender) 23 | [![Coverage status](https://img.shields.io/codecov/c/github/ropenscilabs/defender/master.svg)](https://codecov.io/github/ropenscilabs/defender?branch=master) 24 | [![Lifecycle Status](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/) 25 | 26 | 27 | The goal of defender is to do static code analysis on other R packages to check for potential security risks and best practices. It provides checks on multiple levels: 28 | 29 | 1. [x] static code analysis without installing the package 30 | 2. [ ] more thorough but potentially dangerous checks with installation / in Docker container 31 | 32 | The checks do not tell you whether something is harmful but rather they flag code that you should double-check before running / loading the package. 33 | 34 | ## Installation 35 | 36 | You can install defender from github with: 37 | 38 | ```{r gh-installation, eval = FALSE} 39 | # install.packages("devtools") 40 | devtools::install_github("ropenscilabs/defender") 41 | ``` 42 | 43 | ## Example 44 | 45 | ### System calls in R scripts 46 | 47 | You can check for system calls in any directory locally available: 48 | 49 | ```{r system-calls-example, eval = FALSE} 50 | defender::summarize_system_calls("../testevil") 51 | ``` 52 | 53 | You can also include additional elements to flag as dangerous: 54 | 55 | ```{r system-calls-example-2, eval = FALSE} 56 | sc <- defender::system_calls("poll") 57 | defender::summarize_system_calls("../testevil", calls_to_flag = sc) 58 | ``` 59 | 60 | ### System-related imports in NAMESPACE 61 | 62 | You can check the NAMESPACE file in a package for dangerous imports: 63 | 64 | ```{r namespace-example, eval = FALSE} 65 | defender::check_namespace("../testevil") 66 | ``` 67 | You can also include additional elements to flag as dangerous: 68 | 69 | ```{r namespace-example-2, eval = FALSE} 70 | di <- defender::dangerous_imports("processx::poll") 71 | defender::check_namespace("../testevil", imports_to_flag = di) 72 | ``` 73 | 74 | ## Collaborators 75 | 76 | - Ildi Czeller @czeildi 77 | - Karthik Ram @karthik 78 | - Bob Rudis @hrbrmstr 79 | - Kara Woo @karawoo 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # defender 2 | 3 | [![Project Status: Abandoned](https://www.repostatus.org/badges/latest/abandoned.svg)](https://www.repostatus.org/#abandoned) 4 | 5 | This repository has been archived. The former README is now in [README-NOT.md](README-NOT.md). 6 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://docs.ropensci.org/defender/ 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /codemeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://doi.org/10.5063/schema/codemeta-2.0", 4 | "http://schema.org" 5 | ], 6 | "@type": "SoftwareSourceCode", 7 | "identifier": "defender", 8 | "description": "Check an R package for potential security risks and violations \n via static code analysis.", 9 | "name": "defender: Check Package for Potential Security Violations", 10 | "codeRepository": "https://github.com/ropenscilabs/defender", 11 | "issueTracker": "https://github.com/ropenscilabs/defender/issues", 12 | "license": "https://spdx.org/licenses/MIT", 13 | "version": "0.0.1.9000", 14 | "programmingLanguage": { 15 | "@type": "ComputerLanguage", 16 | "name": "R", 17 | "version": "3.5.0", 18 | "url": "https://r-project.org" 19 | }, 20 | "runtimePlatform": "R version 3.5.0 (2018-04-23)", 21 | "author": [ 22 | { 23 | "@type": "Person", 24 | "givenName": "Ildiko", 25 | "familyName": "Czeller", 26 | "email": "czeildi@gmail.com" 27 | }, 28 | { 29 | "@type": "Person", 30 | "givenName": "Karthik", 31 | "familyName": "Ram", 32 | "email": "karthik.ram@gmail.com", 33 | "@id": "https://orcid.org/ 0000-0002-0233-1757" 34 | }, 35 | { 36 | "@type": "Person", 37 | "givenName": "Bob", 38 | "familyName": "Rudis", 39 | "email": "bob@rud.is", 40 | "@id": "https://orcid.org/0000-0001-5670-2640" 41 | }, 42 | { 43 | "@type": "Person", 44 | "givenName": "Kara", 45 | "familyName": "Woo" 46 | } 47 | ], 48 | "maintainer": [ 49 | { 50 | "@type": "Person", 51 | "givenName": "Ildiko", 52 | "familyName": "Czeller", 53 | "email": "czeildi@gmail.com" 54 | } 55 | ], 56 | "softwareSuggestions": [ 57 | { 58 | "@type": "SoftwareApplication", 59 | "identifier": "covr", 60 | "name": "covr", 61 | "provider": { 62 | "@id": "https://cran.r-project.org", 63 | "@type": "Organization", 64 | "name": "Comprehensive R Archive Network (CRAN)", 65 | "url": "https://cran.r-project.org" 66 | }, 67 | "sameAs": "https://CRAN.R-project.org/package=covr" 68 | }, 69 | { 70 | "@type": "SoftwareApplication", 71 | "identifier": "testthat", 72 | "name": "testthat", 73 | "provider": { 74 | "@id": "https://cran.r-project.org", 75 | "@type": "Organization", 76 | "name": "Comprehensive R Archive Network (CRAN)", 77 | "url": "https://cran.r-project.org" 78 | }, 79 | "sameAs": "https://CRAN.R-project.org/package=testthat" 80 | }, 81 | { 82 | "@type": "SoftwareApplication", 83 | "identifier": "knitr", 84 | "name": "knitr", 85 | "provider": { 86 | "@id": "https://cran.r-project.org", 87 | "@type": "Organization", 88 | "name": "Comprehensive R Archive Network (CRAN)", 89 | "url": "https://cran.r-project.org" 90 | }, 91 | "sameAs": "https://CRAN.R-project.org/package=knitr" 92 | }, 93 | { 94 | "@type": "SoftwareApplication", 95 | "identifier": "roxygen2", 96 | "name": "roxygen2", 97 | "provider": { 98 | "@id": "https://cran.r-project.org", 99 | "@type": "Organization", 100 | "name": "Comprehensive R Archive Network (CRAN)", 101 | "url": "https://cran.r-project.org" 102 | }, 103 | "sameAs": "https://CRAN.R-project.org/package=roxygen2" 104 | }, 105 | { 106 | "@type": "SoftwareApplication", 107 | "identifier": "rstudioapi", 108 | "name": "rstudioapi", 109 | "provider": { 110 | "@id": "https://cran.r-project.org", 111 | "@type": "Organization", 112 | "name": "Comprehensive R Archive Network (CRAN)", 113 | "url": "https://cran.r-project.org" 114 | }, 115 | "sameAs": "https://CRAN.R-project.org/package=rstudioapi" 116 | }, 117 | { 118 | "@type": "SoftwareApplication", 119 | "identifier": "shiny", 120 | "name": "shiny", 121 | "provider": { 122 | "@id": "https://cran.r-project.org", 123 | "@type": "Organization", 124 | "name": "Comprehensive R Archive Network (CRAN)", 125 | "url": "https://cran.r-project.org" 126 | }, 127 | "sameAs": "https://CRAN.R-project.org/package=shiny" 128 | }, 129 | { 130 | "@type": "SoftwareApplication", 131 | "identifier": "miniUI", 132 | "name": "miniUI", 133 | "provider": { 134 | "@id": "https://cran.r-project.org", 135 | "@type": "Organization", 136 | "name": "Comprehensive R Archive Network (CRAN)", 137 | "url": "https://cran.r-project.org" 138 | }, 139 | "sameAs": "https://CRAN.R-project.org/package=miniUI" 140 | } 141 | ], 142 | "softwareRequirements": [ 143 | { 144 | "@type": "SoftwareApplication", 145 | "identifier": "R", 146 | "name": "R", 147 | "version": ">= 3.1.0" 148 | }, 149 | { 150 | "@type": "SoftwareApplication", 151 | "identifier": "magrittr", 152 | "name": "magrittr", 153 | "provider": { 154 | "@id": "https://cran.r-project.org", 155 | "@type": "Organization", 156 | "name": "Comprehensive R Archive Network (CRAN)", 157 | "url": "https://cran.r-project.org" 158 | }, 159 | "sameAs": "https://CRAN.R-project.org/package=magrittr" 160 | }, 161 | { 162 | "@type": "SoftwareApplication", 163 | "identifier": "utils", 164 | "name": "utils" 165 | }, 166 | { 167 | "@type": "SoftwareApplication", 168 | "identifier": "withr", 169 | "name": "withr", 170 | "provider": { 171 | "@id": "https://cran.r-project.org", 172 | "@type": "Organization", 173 | "name": "Comprehensive R Archive Network (CRAN)", 174 | "url": "https://cran.r-project.org" 175 | }, 176 | "sameAs": "https://CRAN.R-project.org/package=withr" 177 | } 178 | ], 179 | "releaseNotes": "https://github.com/ropenscilabs/defender/blob/master/NEWS.md", 180 | "readme": "https://github.com/ropenscilabs/defender/blob/master/README.md", 181 | "fileSize": "53.069KB", 182 | "contIntegration": [ 183 | "https://travis-ci.org/ropenscilabs/defender", 184 | "https://codecov.io/github/ropenscilabs/defender?branch=master" 185 | ], 186 | "keywords": [ 187 | "unconf18", 188 | "security", 189 | "r", 190 | "r-package", 191 | "rstats", 192 | "unconf" 193 | ], 194 | "relatedLink": "https://docs.ropensci.org/defender/" 195 | } 196 | -------------------------------------------------------------------------------- /defender.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 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 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /inst/rstudio/addins.dcf: -------------------------------------------------------------------------------- 1 | Name: List system calls 2 | Description: Lists all system calls in given directory in R source files in a data.frame 3 | Binding: system_calls_addin 4 | Interactive: true 5 | 6 | Name: List namespace imports 7 | Description: Lists all system related imports in package 8 | Binding: check_namespace_addin 9 | Interactive: true 10 | -------------------------------------------------------------------------------- /man/check_namespace.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check_namespace.R 3 | \name{check_namespace} 4 | \alias{check_namespace} 5 | \title{Read a package NAMESPACE and check for dangerous imports} 6 | \usage{ 7 | check_namespace(pkg_path, imports_to_flag = dangerous_imports()) 8 | } 9 | \arguments{ 10 | \item{pkg_path}{path to package source tree} 11 | 12 | \item{imports_to_flag}{character vector of dangerous items to find} 13 | } 14 | \description{ 15 | Given a path to a package source tree, return a data.frame of Imports (both whole 16 | packages and fully qualified references). 17 | } 18 | \examples{ 19 | \dontrun{ 20 | check_namespace("../testevil") 21 | check_namespace( 22 | "../testevil", 23 | dangerous_imports(additional_dangerous_imports = "sys::exec_background") 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /man/dangerous_imports.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{dangerous_imports} 4 | \alias{dangerous_imports} 5 | \title{Dangerous Imports in Namespace} 6 | \usage{ 7 | dangerous_imports(additional_dangerous_imports = character(0)) 8 | } 9 | \arguments{ 10 | \item{additional_dangerous_imports}{character vector of user-defined 11 | dangerous imports} 12 | } 13 | \value{ 14 | character vector of all imports considered dangerous 15 | } 16 | \description{ 17 | Character vector of all imports considered dangerous to be used with 18 | \code{\link{check_namespace}}. 19 | } 20 | \examples{ 21 | dangerous_imports() 22 | } 23 | -------------------------------------------------------------------------------- /man/defender-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/defender-package.R 3 | \docType{package} 4 | \name{defender-package} 5 | \alias{defender} 6 | \alias{defender-package} 7 | \title{defender: Check Package for Potential Security Violations} 8 | \description{ 9 | \if{html}{\figure{logo.png}{options: align='right'}} 10 | 11 | Check an R package for potential security risks and violations 12 | via static code analysis. 13 | } 14 | \seealso{ 15 | Useful links: 16 | \itemize{ 17 | \item \url{https://github.com/ropenscilabs/defender} 18 | \item Report bugs at \url{https://github.com/ropenscilabs/defender/issues} 19 | } 20 | 21 | } 22 | \author{ 23 | \strong{Maintainer}: Ildiko Czeller \email{czeildi@gmail.com} 24 | 25 | Authors: 26 | \itemize{ 27 | \item Karthik Ram \email{karthik.ram@gmail.com} ( 0000-0002-0233-1757) 28 | \item Bob Rudis \email{bob@rud.is} (0000-0001-5670-2640) 29 | \item Kara Woo 30 | } 31 | 32 | } 33 | \keyword{internal} 34 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ropensci-archive/defender/d9218d5ec84227269833b8b5333e44906d40cd2f/man/figures/logo.png -------------------------------------------------------------------------------- /man/figures/supergb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ropensci-archive/defender/d9218d5ec84227269833b8b5333e44906d40cd2f/man/figures/supergb.png -------------------------------------------------------------------------------- /man/summarize_system_calls.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/system_calls.R 3 | \name{summarize_system_calls} 4 | \alias{summarize_system_calls} 5 | \title{summarize occurrences of system calls in project} 6 | \usage{ 7 | summarize_system_calls(path = ".", calls_to_flag = system_calls()) 8 | } 9 | \arguments{ 10 | \item{path}{character, relative or absolute path for local code directory} 11 | 12 | \item{calls_to_flag}{character vector of functions names to flag as 13 | potentially dangerous system calls} 14 | } 15 | \value{ 16 | data.frame with a row for each system call 17 | } 18 | \description{ 19 | Summarize and show occurrences of system calls in the R files in a given 20 | folder. There could be more occurrences. These calls should be further checked 21 | for maliciousness. 22 | } 23 | \examples{ 24 | \dontrun{ 25 | # git clone git@github.com:ropenscilabs/testevil.git 26 | summarize_system_calls("testevil") 27 | summarize_system_calls("testevil", system_calls("exec_background")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /man/system_calls.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{system_calls} 4 | \alias{system_calls} 5 | \title{Functions making System Calls} 6 | \usage{ 7 | system_calls(additional_system_calls = character(0)) 8 | } 9 | \arguments{ 10 | \item{additional_system_calls}{character vector of user-defined 11 | system calls} 12 | } 13 | \value{ 14 | character vector of all system calls 15 | } 16 | \description{ 17 | Character vector of functions making system calls to check for in R source 18 | files. 19 | } 20 | \examples{ 21 | system_calls("exec_background") 22 | } 23 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(defender) 3 | 4 | test_check("defender") 5 | -------------------------------------------------------------------------------- /tests/testthat/test-check_namespace.R: -------------------------------------------------------------------------------- 1 | context("test-check_namespace.R") 2 | 3 | setup({ 4 | td <- file.path(normalizePath("."), "testdir") 5 | unlink(td, recursive = TRUE) 6 | dir.create(td) 7 | }) 8 | 9 | teardown({ 10 | td <- file.path(normalizePath("."), "testdir") 11 | unlink(td, recursive = TRUE) 12 | }) 13 | 14 | test_that("parse fqrs from nested list to vector", { 15 | expect_equal( 16 | transform_fully_qualified_refereces( 17 | list(list("processx", "run"), list("httr", "verbose")) 18 | ), 19 | c("processx::run", "httr::verbose") 20 | ) 21 | }) 22 | 23 | test_that("extract packges from fully qualified references", { 24 | expect_equal( 25 | extract_pkgs_from_fully_qualified_references( 26 | list(list("processx", "run"), list("httr", "verbose")) 27 | ), 28 | c("processx", "httr") 29 | ) 30 | }) 31 | 32 | test_that("extract fqrs from all imports", { 33 | expect_equal( 34 | extract_fully_qualified_references( 35 | list("sys", list("processx", "run"), "httr") 36 | ), 37 | list(list("processx", "run")) 38 | ) 39 | }) 40 | 41 | test_that("extract whole package imports", { 42 | expect_equal( 43 | extract_whole_pkg_imports( 44 | list(list("sys"), list("processx", "run"), list("httr")) 45 | ), 46 | c("sys", "httr") 47 | ) 48 | }) 49 | 50 | test_that("parse namespace file", { 51 | td <- file.path(normalizePath("."), "testdir") 52 | unlink(td, recursive = TRUE) 53 | dir.create(td) 54 | file.create(file.path(td, "NAMESPACE")) 55 | expect_true( 56 | all(c("imports", "exports") %in% names(parse_ns_file(td))) 57 | ) 58 | unlink(td, recursive = TRUE) 59 | }) 60 | 61 | test_that("summarize imports handles empty import list", { 62 | expect_silent(summarize_imports(list(), list())) 63 | }) 64 | 65 | test_that("summarize imports returns data frame", { 66 | expect_is( 67 | summarize_imports("sys", "processx::run"), 68 | "data.frame" 69 | ) 70 | expect_named( 71 | summarize_imports("sys", "processx::run"), 72 | c("type", "import", "package") 73 | ) 74 | }) 75 | 76 | test_that("parse all imports by type", { 77 | expect_named( 78 | parse_all_imports( 79 | list(list("sys"), list("processx", "run"), list("httr")) 80 | ), 81 | c("imported_packages", "imported_functions") 82 | ) 83 | }) 84 | 85 | test_that("check namespace fails if path is not package", { 86 | expect_error(check_namespace(tempdir())) 87 | expect_error(check_namespace("nonexistent/path")) 88 | }) 89 | 90 | test_that("check namespace returns only flagged imports", { 91 | td <- file.path(normalizePath("."), "testdir") 92 | unlink(td, recursive = TRUE) 93 | dir.create(td) 94 | file.create(file.path(td, "DESCRIPTION")) 95 | writeLines("import(magrittr)", file.path(td, "NAMESPACE")) 96 | expect_equal( 97 | nrow(check_namespace(td, imports_to_flag = "withr")), 98 | 0 99 | ) 100 | expect_equal( 101 | nrow(check_namespace(td, imports_to_flag = "magrittr")), 102 | 1 103 | ) 104 | unlink(td, recursive = TRUE) 105 | }) 106 | -------------------------------------------------------------------------------- /tests/testthat/test-system_calls.R: -------------------------------------------------------------------------------- 1 | context("test-system_calls.R") 2 | 3 | setup({ 4 | td <- file.path(normalizePath("."), "testdir") 5 | unlink(td, recursive = TRUE) 6 | dir.create(td) 7 | }) 8 | 9 | teardown({ 10 | unlink(td, recursive = TRUE) 11 | }) 12 | 13 | test_that("expression is parsed to data frame", { 14 | expect_s3_class( 15 | find_system_calls(parse(text = "system2()")), 16 | "data.frame" 17 | ) 18 | }) 19 | 20 | 21 | test_that("find system calls does not return plain functions calls", { 22 | expect_equal( 23 | nrow(find_system_calls(parse(text = "foo()\nsystem2()\nsystem()"))), 24 | 2 25 | ) 26 | }) 27 | 28 | 29 | test_that("informative error message if directory is not found", { 30 | expect_error( 31 | summarize_system_calls(file.path("non", "existent", "path")), 32 | "Path non/existent/path not found, you may want to clone the repository first." 33 | ) 34 | }) 35 | 36 | 37 | test_that("summarize system calls returns one data frame", { 38 | td <- file.path(normalizePath("."), "testdir") 39 | unlink(td, recursive = TRUE) 40 | dir.create(td) 41 | writeLines("sys::run()", file.path(td, "test.R")) 42 | expect_is( 43 | summarize_system_calls(td), 44 | "data.frame" 45 | ) 46 | expect_is( 47 | summarize_system_calls(td, "sys::run"), 48 | "data.frame" 49 | ) 50 | unlink(td, recursive = TRUE) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | context("test-utils.R") 2 | 3 | setup({ 4 | td <- file.path(normalizePath("."), "testdir") 5 | unlink(td, recursive = TRUE) 6 | dir.create(td) 7 | }) 8 | 9 | teardown({ 10 | td <- file.path(normalizePath("."), "testdir") 11 | unlink(td, recursive = TRUE) 12 | }) 13 | 14 | test_that("throw error iff path does not point to a package", { 15 | td <- file.path(normalizePath("."), "testdir") 16 | unlink(td, recursive = TRUE) 17 | dir.create(td) 18 | expect_error(assert_is_package(td)) 19 | file.create(file.path(td, "DESCRIPTION")) 20 | expect_silent(assert_is_package(td)) 21 | unlink(td, recursive = TRUE) 22 | }) 23 | 24 | test_that("find R scripts in given directory", { 25 | td <- file.path(normalizePath("."), "testdir") 26 | unlink(td, recursive = TRUE) 27 | dir.create(td) 28 | dir.create(file.path(td, "inner")) 29 | file.create(file.path(td, "first.r")) 30 | file.create(file.path(td, "second.R")) 31 | file.create(file.path(td, "third.h")) 32 | file.create(file.path(td, "inner", "fourth.R")) 33 | expect_length( 34 | get_r_script_paths(td), 3 35 | ) 36 | unlink(td, recursive = TRUE) 37 | }) 38 | 39 | test_that("return full path of found R scripts", { 40 | td <- file.path(normalizePath("."), "testdir") 41 | unlink(td, recursive = TRUE) 42 | dir.create(td) 43 | file.create(file.path(td, "first.r")) 44 | expect_false("first.r" %in% get_r_script_paths(td)) 45 | unlink(td, recursive = TRUE) 46 | }) 47 | 48 | test_that("return main packages doing system calls", { 49 | expect_setequal( 50 | c("sys", "processx"), 51 | pkgs_doing_system_calls() 52 | ) 53 | }) 54 | 55 | test_that("return union of built-in and user-defined dangerous imports", { 56 | expect_equal( 57 | dangerous_imports(c("testevil::evil", "processx::run")), 58 | c("sys", "processx", "processx::run", "testevil::evil") 59 | ) 60 | }) 61 | --------------------------------------------------------------------------------