defmodule GitGud.Audit.Catalog do
@moduledoc """
Every action the audit log knows, and the only place one exists.
Each action names its category (what the log is filtered by), the
kind of thing it acts on, and the metadata keys it may keep. Keys not
listed are dropped when the entry is built — that list is what stands
between a careless call site and a secret value in the table. Never
list a key that could hold a secret, a token, a webhook URL's path, or
a hash of any of them.
Actions are added with the slice that records them
(`docs/plans/org-audit-log.md`).
"""
@actions %{
# CI secrets and variables. The name is the target label; values
# never appear.
"repo_secret.set" => %{category: "secrets", target: "secret", keys: ~w()},
"repo_secret.deleted" => %{category: "secrets", target: "secret", keys: ~w()},
"repo_variable.set" => %{category: "secrets", target: "variable", keys: ~w()},
"repo_variable.deleted" => %{category: "secrets", target: "variable", keys: ~w()},
"org_secret.set" => %{category: "secrets", target: "secret", keys: ~w()},
"org_secret.deleted" => %{category: "secrets", target: "secret", keys: ~w()},
"org_variable.set" => %{category: "secrets", target: "variable", keys: ~w()},
"org_variable.deleted" => %{category: "secrets", target: "variable", keys: ~w()},
# Membership. The person invited, promoted or removed is the
# entry's subject and its target label (their handle).
"org.invitation_sent" => %{category: "members", target: "invitation", keys: ~w(role)},
"org.invitation_withdrawn" => %{category: "members", target: "invitation", keys: ~w()},
"org.invitation_accepted" => %{category: "members", target: "invitation", keys: ~w(role)},
"org.invitation_declined" => %{category: "members", target: "invitation", keys: ~w()},
"org.member_role_changed" => %{category: "members", target: "membership", keys: ~w(from to)},
"org.member_removed" => %{category: "members", target: "membership", keys: ~w(role)},
"team.created" => %{category: "members", target: "team", keys: ~w()},
# The organization itself. `fields` names what a profile edit
# changed, never the new values.
"org.created" => %{category: "settings", target: "organization", keys: ~w(visibility)},
"org.profile_updated" => %{category: "settings", target: "organization", keys: ~w(fields)},
"org.visibility_changed" => %{
category: "settings",
target: "organization",
keys: ~w(from to)
},
# Repositories. Created, forked and deleted ones keep their
# `owner/name` as the repository label after they're gone. An
# import's source is its host only: the URL's path and userinfo stay
# out.
"repo.created" => %{
category: "repositories",
target: "repository",
keys: ~w(visibility imported_from_host forked_from)
},
"repo.forked" => %{category: "repositories", target: "repository", keys: ~w(destination)},
"repo.deleted" => %{category: "repositories", target: "repository", keys: ~w()},
"repo.branch_overwritten" => %{
category: "repositories",
target: "branch",
keys: ~w(from to)
},
# A settings save: each changed field as "old → new", except the
# description, which is only "changed".
"repo.settings_updated" => %{
category: "settings",
target: "repository",
keys: ~w(description default_branch required_approvals block_merge_on_changes_requested
block_self_approval public_package_formats)
},
"repo.visibility_changed" => %{category: "settings", target: "repository", keys: ~w(from to)},
"repo.archived" => %{category: "settings", target: "repository", keys: ~w()},
"repo.unarchived" => %{category: "settings", target: "repository", keys: ~w()},
"repo.upstream_settings_changed" => %{
category: "settings",
target: "repository",
keys: ~w(mode auto)
},
"repo.interaction_policy_changed" => %{
category: "settings",
target: "repository",
keys: ~w(from to)
},
# Repository controls. A webhook is labelled by its host alone:
# Slack- and Discord-style URLs carry their credential in the path.
# Never a deploy key's body, a token or a webhook secret.
"branch_protection.created" => %{
category: "branch_protection",
target: "branch_protection",
keys:
~w(required_approvals allow_force_push allow_deletions require_status_checks required_checks)
},
"branch_protection.deleted" => %{
category: "branch_protection",
target: "branch_protection",
keys: ~w()
},
"deploy_key.added" => %{
category: "deploy_keys",
target: "deploy_key",
keys: ~w(fingerprint read_write)
},
"deploy_key.removed" => %{
category: "deploy_keys",
target: "deploy_key",
keys: ~w(fingerprint)
},
"webhook.created" => %{
category: "webhooks",
target: "webhook",
keys: ~w(events content_type insecure_ssl)
},
"webhook.deleted" => %{category: "webhooks", target: "webhook", keys: ~w()},
"webhook.enabled" => %{category: "webhooks", target: "webhook", keys: ~w()},
"webhook.disabled" => %{category: "webhooks", target: "webhook", keys: ~w()},
"registry_token.created" => %{
category: "credentials",
target: "registry_token",
keys: ~w(scopes)
},
"registry_token.revoked" => %{category: "credentials", target: "registry_token", keys: ~w()},
"job_token_grant.created" => %{
category: "credentials",
target: "job_token_grant",
keys: ~w(read dispatch)
},
"job_token_grant.deleted" => %{
category: "credentials",
target: "job_token_grant",
keys: ~w()
},
# Runners: never a registration token or a runner's own token.
"runner.registration_token_minted" => %{
category: "runners",
target: nil,
keys: ~w(expires_at)
},
"runner.registered" => %{category: "runners", target: "runner", keys: ~w(labels version)},
"runner.created" => %{category: "runners", target: "runner", keys: ~w(labels)},
"runner.deleted" => %{category: "runners", target: "runner", keys: ~w()},
# Approving a fork's runs lets code nobody here wrote run on the
# repository's runners.
"ci.fork_runs_approved" => %{
category: "ci",
target: "pull_request",
keys: ~w(pull_request runs)
},
"ci.fork_runs_rejected" => %{
category: "ci",
target: "pull_request",
keys: ~w(pull_request runs)
},
# Moderation. A ban's reason is the moderator's own words, already
# shown on the ban list. Bans keep their trail on the ban row too;
# this puts them in the scope's log beside everything else.
"ban.created" => %{category: "moderation", target: "ban", keys: ~w(reason expires_at)},
"ban.lifted" => %{category: "moderation", target: "ban", keys: ~w()},
"report.dismissed" => %{category: "moderation", target: "report", keys: ~w(target_type)},
"report.actioned" => %{category: "moderation", target: "report", keys: ~w(target_type)},
"report.content_moderated" => %{
category: "moderation",
target: "report",
keys: ~w(target_type)
},
# The instance: federation, instance settings, registration invites.
# These belong to no org or repository, and show only on the
# instance view. A remote actor is labelled by its public URL.
"instance.blocked" => %{category: "instance", target: "instance", keys: ~w(severity reason)},
"instance.unblocked" => %{category: "instance", target: "instance", keys: ~w()},
"instance.allowlisted" => %{category: "instance", target: "instance", keys: ~w()},
"instance.unallowlisted" => %{category: "instance", target: "instance", keys: ~w()},
"suggested_block.approved" => %{category: "instance", target: "instance", keys: ~w()},
"suggested_block.dismissed" => %{category: "instance", target: "instance", keys: ~w()},
"remote_actor.blocked" => %{category: "instance", target: "remote_actor", keys: ~w(reason)},
"remote_actor.unblocked" => %{category: "instance", target: "remote_actor", keys: ~w()},
"remote_actor.suspended" => %{category: "instance", target: "remote_actor", keys: ~w()},
"inbox.quarantine_approved" => %{
category: "instance",
target: "inbox_delivery",
keys: ~w(source_actor)
},
"inbox.quarantine_rejected" => %{
category: "instance",
target: "inbox_delivery",
keys: ~w(source_actor)
},
"report.forwarded" => %{category: "moderation", target: "report", keys: ~w(target_type)},
"report.content_restored" => %{
category: "moderation",
target: "report",
keys: ~w(target_type)
},
"instance_setting.changed" => %{
category: "instance",
target: "instance_setting",
keys: ~w(from to)
},
"packages.pruned" => %{
category: "instance",
target: nil,
keys: ~w(versions bytes failed)
},
# A registration invite, by id: its link is the credential.
"invite.created" => %{category: "instance", target: "invite", keys: ~w()},
"invite.revoked" => %{category: "instance", target: "invite", keys: ~w()},
# A person's own account: the security log at
# /users/settings/security-log. Each is about the user themselves
# (the subject), with no org or repository. Never a password, code,
# key body, token or email address.
"session.login" => %{category: "account", target: nil, keys: ~w(method)},
"session.login_failed" => %{category: "account", target: nil, keys: ~w()},
"session.logout" => %{category: "account", target: nil, keys: ~w()},
"sudo.entered" => %{category: "account", target: nil, keys: ~w(method)},
"password.changed" => %{category: "account", target: nil, keys: ~w()},
"email.changed" => %{category: "account", target: nil, keys: ~w()},
"two_factor.enabled" => %{category: "account", target: nil, keys: ~w()},
"two_factor.disabled" => %{category: "account", target: nil, keys: ~w()},
"two_factor.reauth_changed" => %{category: "account", target: nil, keys: ~w(enabled)},
"recovery_codes.regenerated" => %{category: "account", target: nil, keys: ~w()},
"webauthn.added" => %{category: "account", target: "webauthn_credential", keys: ~w()},
"webauthn.removed" => %{category: "account", target: "webauthn_credential", keys: ~w()},
"pat.created" => %{category: "account", target: "personal_access_token", keys: ~w(scopes)},
"pat.revoked" => %{category: "account", target: "personal_access_token", keys: ~w()},
"ssh_key.added" => %{category: "account", target: "ssh_key", keys: ~w(fingerprint)},
"ssh_key.removed" => %{category: "account", target: "ssh_key", keys: ~w(fingerprint)},
# The pruner's own trace: how much went, from before when.
"audit.pruned" => %{category: "instance", target: nil, keys: ~w(count cutoff)}
}
@categories @actions |> Map.values() |> Enum.map(& &1.category) |> Enum.uniq() |> Enum.sort()
@doc "The action's definition, or nil for an action that doesn't exist."
@spec fetch(String.t()) ::
%{category: String.t(), target: String.t() | nil, keys: [String.t()]} | nil
def fetch(action) when is_binary(action), do: Map.get(@actions, action)
def fetch(_), do: nil
@doc "Every action name."
def actions, do: Map.keys(@actions)
@doc "Every category an action belongs to."
def categories, do: @categories
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
11.2 KiB · text
History
4770085