Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sonarqube formatter, extract formatter as abstract behavior #107

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions lib/mix/tasks/sobelow.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule Mix.Tasks.Sobelow do
* `--compact` - Minimal, single-line findings
* `--save-config` - Generates a configuration file based on command line options
* `--config` - Run Sobelow with configuration file
* `--sonarqube-base-folder` - Prefix for sonarqube files

## Ignoring modules

Expand Down Expand Up @@ -105,7 +106,8 @@ defmodule Mix.Tasks.Sobelow do
compact: :boolean,
flycheck: :boolean,
out: :string,
threshold: :string
threshold: :string,
sonarqube_base_folder: :string
]

@aliases [v: :verbose, r: :root, i: :ignore, d: :details, f: :format]
Expand Down Expand Up @@ -140,7 +142,7 @@ defmodule Mix.Tasks.Sobelow do

{verbose, diff, details, private, strict, skip, mark_skip_all, clear_skip, router, exit_on,
format, ignored, ignored_files, all_details, out,
threshold} = get_opts(opts, root, conf_file?)
threshold, sonarqube_base_folder} = get_opts(opts, root, conf_file?)

set_env(:verbose, verbose)

Expand All @@ -163,6 +165,7 @@ defmodule Mix.Tasks.Sobelow do
set_env(:ignored_files, ignored_files)
set_env(:out, out)
set_env(:threshold, threshold)
set_env(:sonarqube_base_folder, sonarqube_base_folder)

save_config = Keyword.get(opts, :save_config)

Expand Down Expand Up @@ -212,6 +215,7 @@ defmodule Mix.Tasks.Sobelow do
clear_skip = Keyword.get(opts, :clear_skip, false)
router = Keyword.get(opts, :router)
out = Keyword.get(opts, :out)
sonarqube_base_folder = Keyword.get(opts, :sonarqube_base_folder)

exit_on =
Keyword.get(opts, :exit, "None")
Expand Down Expand Up @@ -263,7 +267,7 @@ defmodule Mix.Tasks.Sobelow do
end

{verbose, diff, details, private, strict, skip, mark_skip_all, clear_skip, router, exit_on,
format, ignored, ignored_files, all_details, out, threshold}
format, ignored, ignored_files, all_details, out, threshold, sonarqube_base_folder}
end

# Future updates will include format hinting based on the outfile name. Additional output
Expand All @@ -273,7 +277,7 @@ defmodule Mix.Tasks.Sobelow do

defp out_format(_out, format) do
cond do
format in ["json", "quiet", "sarif"] -> format
format in ["json", "quiet", "sarif", "sonarqube"] -> format
true -> "json"
end
end
Expand Down
20 changes: 4 additions & 16 deletions lib/sobelow.ex
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ defmodule Sobelow do
# - Remove config check from "allowed" modules
# - Scan funcs from the root
# - Scan funcs from the libroot
if not (format() in ["quiet", "compact", "flycheck", "json"]),
if not (format() in ["quiet", "compact", "flycheck", "json", "sonarqube"]),
do: IO.puts(:stderr, print_banner())

Application.put_env(:sobelow, :app_name, app_name)
Expand Down Expand Up @@ -136,20 +136,7 @@ defmodule Sobelow do
end

defp print_output() do
details =
case output_format() do
"json" ->
FindingLog.json(@v)

"quiet" ->
FindingLog.quiet()

"sarif" ->
FindingLog.sarif(@v)

_ ->
nil
end
details = output_format() |> FindingLog.formatted_output(@v)

if !is_nil(details) do
print_std_or_file(details)
Expand Down Expand Up @@ -248,7 +235,8 @@ defmodule Sobelow do
out: get_env(:out),
threshold: get_env(:threshold),
ignore: get_env(:ignored),
ignore_files: get_env(:ignored_files)
ignore_files: get_env(:ignored_files),
sonarqube_base_folder: get_env(:sonarqube_base_folder)
]

yes? =
Expand Down
126 changes: 6 additions & 120 deletions lib/sobelow/finding_log.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule Sobelow.FindingLog do
use GenServer
alias Sobelow.Formatter.{SonarQube, Json, Sarif, Quiet}

def start_link() do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
Expand All @@ -13,69 +14,11 @@ defmodule Sobelow.FindingLog do
GenServer.call(__MODULE__, :log)
end

def json(vsn) do
%{high: highs, medium: meds, low: lows} = log()
highs = normalize_json_log(highs)
meds = normalize_json_log(meds)
lows = normalize_json_log(lows)

Jason.encode!(
format_json(%{
findings: %{high_confidence: highs, medium_confidence: meds, low_confidence: lows},
total_findings: length(highs) + length(meds) + length(lows),
sobelow_version: vsn
}),
pretty: true
)
end

def sarif(vsn) do
Jason.encode!(
%{
version: "2.1.0",
"$schema":
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
runs: [
%{
tool: %{
driver: %{
name: "Sobelow",
informationUri: "https://sobelow.io",
semanticVersion: vsn,
rules: Sobelow.rules()
}
},
results: sarif_results()
}
]
},
pretty: true
)
end

def sarif_results() do
%{high: highs, medium: meds, low: lows} = log()

highs = normalize_sarif_log(highs)
meds = normalize_sarif_log(meds)
lows = normalize_sarif_log(lows)

Enum.map(highs, &format_sarif/1) ++
Enum.map(meds, &format_sarif/1) ++ Enum.map(lows, &format_sarif/1)
end

def quiet() do
total = total(log())
findings = if total > 1, do: "findings", else: "finding"

if total > 0 do
"Sobelow: #{total} #{findings} found. Run again without --quiet to review findings."
end
end

defp total(%{high: highs, medium: meds, low: lows}) do
length(highs) + length(meds) + length(lows)
end
def formatted_output("sonarqube", vsn), do: SonarQube.format_findings(log(), vsn)
def formatted_output("json", vsn), do: Json.format_findings(log(), vsn)
def formatted_output("sarif", vsn), do: Sarif.format_findings(log(), vsn)
def formatted_output("quiet", vsn), do: Quiet.format_findings(log(), vsn)
def formatted_output(_, _vsn), do: nil

def init(:ok) do
{:ok, %{:high => [], :medium => [], :low => []}}
Expand All @@ -89,61 +32,4 @@ defmodule Sobelow.FindingLog do
{:reply, findings, findings}
end

def format_json(map) when is_map(map) do
map |> Enum.map(fn {k, v} -> {k, format_json(v)} end) |> Enum.into(%{})
end

def format_json(l) when is_list(l) do
l |> Enum.map(&format_json(&1))
end

def format_json({_, _, _} = var) do
details = {var, [], []} |> Macro.to_string()
"\"#{details}\""
end

def format_json(n), do: n

defp format_sarif(finding) do
[mod, _] = String.split(finding.type, ":", parts: 2)

%{
ruleId: Sobelow.get_mod(mod).id,
message: %{
text: finding.type
},
locations: [
%{
physicalLocation: %{
artifactLocation: %{
uri: finding.filename
},
region: %{
startLine: sarif_num(finding.vuln_line_no),
startColumn: sarif_num(finding.vuln_col_no),
endLine: sarif_num(finding.vuln_line_no),
endColumn: sarif_num(finding.vuln_col_no)
}
}
}
],
partialFingerprints: %{
primaryLocationLineHash: finding.fingerprint
},
level: to_level(finding.confidence)
}
end

defp to_level(:high), do: "error"
defp to_level(_), do: "warning"

defp sarif_num(0), do: 1
defp sarif_num(num), do: num

defp normalize_json_log(finding), do: finding |> Stream.map(fn {d, _} -> d end) |> normalize()

defp normalize_sarif_log(finding),
do: finding |> Stream.map(fn {_, f} -> Map.from_struct(f) end) |> normalize()

defp normalize(l), do: l |> Enum.map(&Map.new/1)
end
8 changes: 8 additions & 0 deletions lib/sobelow/formatter/formatter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Sobelow.Formatter do
@moduledoc false
@type finding :: %Sobelow.Finding{}
@type log :: %{high: [finding], medium: [finding], low: [finding]}

@callback format_findings(log :: log, String.t) :: term

end
41 changes: 41 additions & 0 deletions lib/sobelow/formatter/json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule Sobelow.Formatter.Json do
@moduledoc false
alias Sobelow.Formatter
@behaviour Formatter

@impl Formatter
def format_findings(%{high: highs, medium: meds, low: lows}, vsn) do
highs = normalize_json_log(highs)
meds = normalize_json_log(meds)
lows = normalize_json_log(lows)

Jason.encode!(
format_json(%{
findings: %{high_confidence: highs, medium_confidence: meds, low_confidence: lows},
total_findings: length(highs) + length(meds) + length(lows),
sobelow_version: vsn
}),
pretty: true
)
end

defp format_json(map) when is_map(map) do
map |> Enum.map(fn {k, v} -> {k, format_json(v)} end) |> Enum.into(%{})
end

defp format_json(l) when is_list(l) do
l |> Enum.map(&format_json(&1))
end

defp format_json({_, _, _} = var) do
details = {var, [], []} |> Macro.to_string()
"\"#{details}\""
end

defp format_json(n), do: n

defp normalize_json_log(finding), do: finding |> Stream.map(fn {d, _} -> d end) |> normalize()

defp normalize(l), do: l |> Enum.map(&Map.new/1)

end
20 changes: 20 additions & 0 deletions lib/sobelow/formatter/quiet.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Sobelow.Formatter.Quiet do
@moduledoc false
alias Sobelow.Formatter
@behaviour Formatter

@impl Formatter
def format_findings(%{high: _highs, medium: _meds, low: _lows} = log, _vsn) do
total = total(log)
findings = if total > 1, do: "findings", else: "finding"

if total > 0 do
"Sobelow: #{total} #{findings} found. Run again without --quiet to review findings."
end
end

defp total(%{high: highs, medium: meds, low: lows}) do
length(highs) + length(meds) + length(lows)
end

end
Loading