~hrbrmstr/attckr

71c2b1b087067e461337fb565c6a6b49935e711e — hrbrmstr 10 months ago ab8cfb1
attck_map()
M DESCRIPTION => DESCRIPTION +4 -1
@@ 25,7 25,10 @@ Suggests:
    testthat,
    covr
Depends:
    R (>= 3.2.0)
    R (>= 3.2.0),
    grid,
    gtable,
    ggplot2
Imports:
    jsonlite,
    tibble,

M NAMESPACE => NAMESPACE +7 -0
@@ 1,19 1,26 @@
# Generated by roxygen2: do not edit by hand

export(attck_map)
export(fct_tactic)
export(read_events)
export(validate_tactics)
export(validate_technique_ids)
export(validate_techniques)
import(ggplot2)
import(grid)
import(gtable)
import(rmarkdown)
import(shiny)
import(stringi)
import(tibble)
importFrom(dplyr,arrange)
importFrom(dplyr,case_when)
importFrom(dplyr,count)
importFrom(dplyr,distinct)
importFrom(dplyr,filter)
importFrom(dplyr,group_by)
importFrom(dplyr,mutate)
importFrom(dplyr,n)
importFrom(dplyr,select)
importFrom(glue,glue)
importFrom(jsonlite,fromJSON)

A R/attck-map.R => R/attck-map.R +71 -0
@@ 0,0 1,71 @@
#' Generate an ATT&CK heatmap
#'
#' @param xdf a data frame with `tactic`, `technique` and `value` columns.
#'        If no `value` column exists, then the function will assume you
#'        have passed in individual events and will perform a "count"
#'        summarization before generating the heatmap.
#' @param input,output,matrix if both are not `NULL` then they should be
#'        what [fct_tactic()] takes as parameters. Otherwise, the function
#'        will assume that the `tactic` column is already an ordered factor.
#' @param tile_col,tile_size color/size for the tile borders;
#'        defaults to "`white`" and `0.5`, respectively.
#' @param dark_lab,light_lab text colors for when they appear on top of a
#'        dark or light tile
#' @param dark_value_threshold since you can supply your own fill scale
#'        and may use a transformation (e.g. "`log10`") when doing so, you
#'        can specify the cutoff value for when to use `dark_lab` vs `light_lab`.
#'        If `NULL` then half of `max(value)` will be used.
#' @param ... passed on to the internal call to [ggplot2::geom_text()]
#' @return a ggplot2 plot object which you can add a fill scale to as well
#'         as themeing.
#' @export
attck_map <- function(xdf, input = NULL, output = NULL, matrix = NULL,
                      tile_col = "white", tile_size = 0.5,
                      dark_lab = "white", light_lab = "black",
                      dark_value_threshold = NULL, ...) {

  cn <- colnames(xdf)
  if (!all(c("tactic", "technique") %in% cn)) {
    stop("'xdf' needs both 'tactic' and 'technique' columns.", call.=FALSE)
  }

  if (!("value" %in% cn)) {
    xdf <- dplyr::count(xdf, tactic, technique, name = "value")
  }

  if (is.null(input) && is.null(output)) {
    if (!is.factor(xdf$tactic)) {
      stop(
        "No 'input'/'output' transformation specified but 'tactic' is not a factor.",
        call.=FALSE
      )
    }
  } else {
    if (sum(c(!is.null(input), !is.null(output), !is.null(matrix))) != 3) {
      stop("Must specify 'input', 'output', and 'matrix' if any one of them is not NULL", call.=FALSE)
    }
    xdf$tactic <- fct_tactic(xdf$tactic, input = input, output = output, matrix = matrix)
  }

  if (is.null(dark_value_threshold)) dark_value_threshold <- max(xdf$value)/2

  xdf <- dplyr::arrange(xdf, value)
  xdf$technique <- factor(gsub(" ", "\n", xdf$technique))
  xdf <- dplyr::group_by(xdf, tactic)
  xdf <- dplyr::mutate(xdf, ids = (n():1))
  xdf <- dplyr::ungroup(xdf)

  gg <- ggplot(xdf, aes(tactic, ids))
  gg <- gg + geom_tile(aes(fill = value), color = tile_col, size = tile_size)
  gg <- gg + geom_text(
    aes(
      label = technique,
      color = I(ifelse(value <= dark_value_threshold, dark_lab, light_lab))
    ), ...
  )
  gg <- gg + scale_x_discrete(expand = c(0, 0), position = "top")
  gg <- gg + scale_y_reverse(expand = c(0, 0))

  gg

}

M R/attckr-package.R => R/attckr-package.R +2 -1
@@ 15,7 15,8 @@
#' @author Bob Rudis (bob@@rud.is)
#' @import tibble ggplot2 shiny rmarkdown stringi
#' @importFrom glue glue
#' @importFrom dplyr filter distinct mutate select case_when
#' @importFrom dplyr filter distinct mutate select case_when group_by arrange count n
#' @importFrom readr read_csv
#' @importFrom jsonlite fromJSON
#' @import ggplot2 grid gtable
"_PACKAGE"
\ No newline at end of file

M README.Rmd => README.Rmd +13 -21
@@ 25,10 25,8 @@ hrbrpkghelpr::describe_ingredients()

The following datasets are included:

```{r datasets, echo=FALSE, results="asis"}
d <- as.data.frame(data(package = "attckr")$results[,c("Item", "Title")], stringsAsFactors=FALSE)
d$Item <- sprintf("`%s`", d$Item)
cat(sprintf("- %s: %s", d$Item, d$Title), sep="\n")
```{r datasets, results='asis', echo=FALSE, cache=FALSE}
hrbrpkghelpr::describe_datasets()
```

## Installation


@@ 41,6 39,7 @@ hrbrpkghelpr::install_block()

```{r lib-ex}
library(attckr)
library(hrbrthemes)
library(tidyverse)

# current version


@@ 55,24 54,17 @@ tidy_attack
```{r events, message=TRUE, fig.width=11, fig.height=6}
events <- read_events(system.file("extdat/sample-incidents.csv.gz", package = "attckr"))

count(head(events, 30), tactic, technique) %>%
  mutate(tactic = fct_tactic(tactic, "pretty", "nl")) %>%
  left_join(
    filter(tidy_attack, matrix == "mitre-attack") %>%
      distinct(id, technique),
    c("technique" = "id")
  ) %>%
  complete(tactic, technique.y) %>%
  mutate(technique.y = factor(technique.y, rev(sort(unique(technique.y))))) %>%
  ggplot(aes(tactic, technique.y)) +
  geom_tile(aes(fill = n), color = "#2b2b2b", size = 0.125) +
  scale_x_discrete(expand = c(0, 0), position = "top") +
  scale_fill_viridis_c(direction = -1, na.value = "white") +
  labs(
    x = NULL, y = NULL
attck_map(
  events, "pretty", "nl", "enterprise",
  dark_value_threshold = 1,
  size = 3, family = font_rc, lineheight = 0.875
) +
  scale_fill_distiller(
    palette = "Spectral", na.value = "white", label = scales::comma, breaks = 1:3
  ) +
  theme_minimal() +
  theme(panel.grid=element_blank())
  labs(x = NULL, y = NULL, fill = NULL) +
  theme_ipsum_rc(grid="") +
  theme(axis.text.y = element_blank())
```

## attckr Metrics

M README.md => README.md +14 -19
@@ 32,6 32,7 @@ CTI Corpus.

The following functions are implemented:

  - `attck_map`: Generate an ATT\&CK heatmap
  - `enterprise_attack`: Enterprise Attack Taxonomy v2.0
  - `fct_tactic`: Make an ordered Tactics factor with optional better
    labelling


@@ 81,6 82,7 @@ NOTE: To use the ‘remotes’ install options you will need to have the

``` r
library(attckr)
library(hrbrthemes)
library(tidyverse)

# current version


@@ 123,24 125,17 @@ events <- read_events(system.file("extdat/sample-incidents.csv.gz", package = "a
## You appear to be using Tactic ids.
## You appear to be using Techinque ids.

count(head(events, 30), tactic, technique) %>%
  mutate(tactic = fct_tactic(tactic, "pretty", "nl")) %>%
  left_join(
    filter(tidy_attack, matrix == "mitre-attack") %>%
      distinct(id, technique),
    c("technique" = "id")
  ) %>%
  complete(tactic, technique.y) %>%
  mutate(technique.y = factor(technique.y, rev(sort(unique(technique.y))))) %>%
  ggplot(aes(tactic, technique.y)) +
  geom_tile(aes(fill = n), color = "#2b2b2b", size = 0.125) +
  scale_x_discrete(expand = c(0, 0), position = "top") +
  scale_fill_viridis_c(direction = -1, na.value = "white") +
  labs(
    x = NULL, y = NULL
attck_map(
  events, "pretty", "nl", "enterprise",
  dark_value_threshold = 1,
  size = 3, family = font_rc, lineheight = 0.875
) +
  scale_fill_distiller(
    palette = "Spectral", na.value = "white", label = scales::comma, breaks = 1:3
  ) +
  theme_minimal() +
  theme(panel.grid=element_blank())
  labs(x = NULL, y = NULL, fill = NULL) +
  theme_ipsum_rc(grid="") +
  theme(axis.text.y = element_blank())
```

<img src="man/figures/README-events-1.png" width="1056" />


@@ 149,8 144,8 @@ count(head(events, 30), tactic, technique) %>%

| Lang | \# Files |  (%) | LoC |  (%) | Blank lines |  (%) | \# Lines |  (%) |
| :--- | -------: | ---: | --: | ---: | ----------: | ---: | -------: | ---: |
| R    |       10 | 0.91 | 203 | 0.86 |          57 | 0.74 |      144 | 0.81 |
| Rmd  |        1 | 0.09 |  32 | 0.14 |          20 | 0.26 |       34 | 0.19 |
| R    |       11 | 0.92 | 245 | 0.91 |          65 | 0.76 |      166 | 0.83 |
| Rmd  |        1 | 0.08 |  24 | 0.09 |          20 | 0.24 |       34 | 0.17 |

## Code of Conduct


A man/attck_map.Rd => man/attck_map.Rd +40 -0
@@ 0,0 1,40 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/attck-map.R
\name{attck_map}
\alias{attck_map}
\title{Generate an ATT&CK heatmap}
\usage{
attck_map(xdf, input = NULL, output = NULL, matrix = NULL,
  tile_col = "white", tile_size = 0.5, dark_lab = "white",
  light_lab = "black", dark_value_threshold = NULL, ...)
}
\arguments{
\item{xdf}{a data frame with \code{tactic}, \code{technique} and \code{value} columns.
If no \code{value} column exists, then the function will assume you
have passed in individual events and will perform a "count"
summarization before generating the heatmap.}

\item{input, output, matrix}{if both are not \code{NULL} then they should be
what \code{\link[=fct_tactic]{fct_tactic()}} takes as parameters. Otherwise, the function
will assume that the \code{tactic} column is already an ordered factor.}

\item{tile_col, tile_size}{color/size for the tile borders;
defaults to "\code{white}" and \code{0.5}, respectively.}

\item{dark_lab, light_lab}{text colors for when they appear on top of a
dark or light tile}

\item{dark_value_threshold}{since you can supply your own fill scale
and may use a transformation (e.g. "\code{log10}") when doing so, you
can specify the cutoff value for when to use \code{dark_lab} vs \code{light_lab}.
If \code{NULL} then half of \code{max(value)} will be used.}

\item{...}{passed on to the internal call to \code{\link[ggplot2:geom_text]{ggplot2::geom_text()}}}
}
\value{
a ggplot2 plot object which you can add a fill scale to as well
as themeing.
}
\description{
Generate an ATT&CK heatmap
}

M man/figures/README-events-1.png => man/figures/README-events-1.png +0 -0