Current section

Files

Jump to
mob_dev lib mix tasks mob.security_scan.log.ex
Raw

lib/mix/tasks/mob.security_scan.log.ex

defmodule Mix.Tasks.Mob.SecurityScan.Log do
@shortdoc "Run mix mob.security_scan and update SECURITY_SCAN.md / SECURITY_HISTORY.md"
@moduledoc """
Scheduled-run helper for the security scan. Designed to be invoked
by cron, GitHub Actions, or any other recurring trigger.
Each run does four things:
1. Runs the full `mix mob.security_scan` against the project.
2. **Overwrites** `SECURITY_SCAN.md` — a current-state snapshot
you can point at to answer "what's the situation right now?".
3. **Prepends** a changelog entry to `SECURITY_HISTORY.md` —
newest at the top — describing what's New / Resolved / Still
present since the last logged run.
4. Updates the JSON state sidecar at `.security_scan/state.json`
so the next run can compute its diff.
Commit all three files. The state file is what makes the changelog
meaningful across machines and CI runs — without it every scheduled
run reports every finding as "new" and the history loses signal.
## Usage
mix mob.security_scan.log # default paths
mix mob.security_scan.log --scan SECURITY.md \\
--history HISTORY.md \\
--state .scan/state.json
mix mob.security_scan.log --strict # exit 1 if any high+ finding
## Suggested cron entry
# daily at 06:00 local
0 6 * * * cd /path/to/project && mix mob.security_scan.log >> /tmp/security_scan.log 2>&1
## Suggested GitHub Actions workflow
name: security-scan
on:
schedule: [{cron: "0 6 * * *"}]
workflow_dispatch:
jobs:
scan:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with: {elixir-version: "1.19", otp-version: "28"}
- run: brew install osv-scanner semgrep flawfinder detekt swiftlint
- run: mix deps.get
- run: mix mob.security_scan.log
- uses: peter-evans/create-pull-request@v6
with:
title: "security: weekly scan update"
commit-message: "security: weekly scan update"
branch: security-scan-update
add-paths: SECURITY_SCAN.md SECURITY_HISTORY.md .security_scan/state.json
"""
use Mix.Task
alias MobDev.SecurityScan
alias MobDev.SecurityScan.{Diff, Formatter, HistoryFormatter, Report, StateFile}
@switches [
scan: :string,
history: :string,
state: :string,
project_root: :string,
strict: :boolean,
skip: :string
]
@impl Mix.Task
def run(args) do
{opts, _, _} = OptionParser.parse(args, strict: @switches)
project_root = opts[:project_root] || File.cwd!()
scan_path = opts[:scan] || Path.join(project_root, "SECURITY_SCAN.md")
history_path = opts[:history] || Path.join(project_root, "SECURITY_HISTORY.md")
state_path = opts[:state] || Path.join(project_root, ".security_scan/state.json")
skip = parse_skip(opts[:skip])
now = DateTime.utc_now()
Mix.shell().info("→ scanning #{project_root}")
report = SecurityScan.run(project_root: project_root, skip: skip)
prev_state = StateFile.load(state_path)
diff = Diff.compute(prev_state, report, now)
Mix.shell().info(
"→ diff: +#{length(diff.new)} new / -#{length(diff.resolved)} resolved / =#{length(diff.still_present)} still present"
)
File.write!(scan_path, Formatter.markdown(report))
Mix.shell().info("→ wrote #{scan_path}")
HistoryFormatter.prepend_to_file(history_path, HistoryFormatter.entry(report, diff, now))
Mix.shell().info("→ updated #{history_path}")
next_state = StateFile.from_report(report, diff, now)
StateFile.save(state_path, next_state)
Mix.shell().info("→ saved #{state_path}")
Report.maybe_exit_strict(report, opts[:strict])
end
defp parse_skip(nil), do: []
defp parse_skip(value) do
value
|> String.split(",", trim: true)
|> Enum.map(&(&1 |> String.trim() |> String.to_atom()))
end
end