Packages
mob_dev
0.5.7
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/security_scan/report.ex
defmodule MobDev.SecurityScan.Report do
@moduledoc """
Aggregate result of a security scan: every layer's `LayerResult`
plus run metadata. The report is the single object handed to
formatters (terminal, JSON, markdown) and the value returned
from `MobDev.SecurityScan.run/1`.
Severity rollup helpers (`severity_counts/1`, `worst_severity/1`)
are colocated here so formatters and the `--strict` exit-code
logic agree on the math.
"""
alias MobDev.SecurityScan.{Finding, LayerResult}
@type t :: %__MODULE__{
started_at: DateTime.t(),
finished_at: DateTime.t() | nil,
project_root: String.t(),
layers: [LayerResult.t()]
}
@derive Jason.Encoder
defstruct started_at: nil,
finished_at: nil,
project_root: nil,
layers: []
@doc "Flatten findings across all layers."
@spec all_findings(t()) :: [Finding.t()]
def all_findings(%__MODULE__{layers: layers}) do
Enum.flat_map(layers, & &1.findings)
end
@doc """
Count findings by severity across the report.
Returns a map keyed by `:critical`, `:high`, `:medium`, `:low`,
`:unknown` — every key is present (zero if no findings at that level).
"""
@spec severity_counts(t()) :: %{Finding.severity() => non_neg_integer()}
def severity_counts(%__MODULE__{} = report) do
base = %{critical: 0, high: 0, medium: 0, low: 0, unknown: 0}
report
|> all_findings()
|> Enum.reduce(base, fn %Finding{severity: sev}, acc ->
Map.update(acc, sev, 1, &(&1 + 1))
end)
end
@doc """
Worst severity present in the report. Returns `:none` when the
report has zero findings.
"""
@spec worst_severity(t()) :: Finding.severity() | :none
def worst_severity(%__MODULE__{} = report) do
counts = severity_counts(report)
cond do
counts.critical > 0 -> :critical
counts.high > 0 -> :high
counts.medium > 0 -> :medium
counts.low > 0 -> :low
counts.unknown > 0 -> :unknown
true -> :none
end
end
@doc "Total wall-clock duration of the scan in milliseconds, or nil if not yet finished."
@spec duration_ms(t()) :: non_neg_integer() | nil
def duration_ms(%__MODULE__{started_at: nil}), do: nil
def duration_ms(%__MODULE__{finished_at: nil}), do: nil
def duration_ms(%__MODULE__{started_at: s, finished_at: f}) do
DateTime.diff(f, s, :millisecond)
end
@doc """
If `strict?` is true and the report contains medium-or-worse findings,
print a message to stderr and `exit({:shutdown, 1})`. Otherwise returns
`:ok`. Shared between `mix mob.security_scan` and its `.log` sibling.
"""
@spec maybe_exit_strict(t(), boolean() | nil) :: :ok
def maybe_exit_strict(_report, nil), do: :ok
def maybe_exit_strict(_report, false), do: :ok
def maybe_exit_strict(report, true) do
case worst_severity(report) do
sev when sev in [:critical, :high, :medium] ->
Mix.shell().error("--strict: #{sev} finding(s) present")
exit({:shutdown, 1})
_ ->
:ok
end
end
end