Skip to content
Merged
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: 6 additions & 6 deletions limacharlie/commands/mailsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,13 @@
_EXPLAIN_CAMPAIGN_ACTION = """\
Sweep an action across every member of a campaign. Requires mailsec.act.

PREVIEWS BY DEFAULT. --confirm <campaign_id> is what turns the preview
into an execution, which is the right default for an operation whose
blast radius is every mailbox that received the attack.
PREVIEWS BY DEFAULT. Copy the preview's member-bound `confirm` token into
--confirm to execute exactly the reviewed member set. A campaign id is not
a confirmation token and is refused by the server.

Examples:
limacharlie mailsec campaign action ec7e273b-... --action quarantine_message
limacharlie mailsec campaign action ec7e273b-... --action quarantine_message --confirm ec7e273b-...
limacharlie mailsec campaign action ec7e273b-... --action quarantine_message --confirm 3c514e...
"""

_EXPLAIN_SENDER_GET = """\
Expand Down Expand Up @@ -666,7 +666,7 @@ def campaign_get(ctx, campaign_id) -> None:
@click.argument("campaign_id")
@click.option("--action", "action_name", required=True, help="The typed action to sweep.")
@click.option("--confirm", default=None,
help="Pass the campaign id to EXECUTE. Omit to preview — previewing is the default.")
help="Pass the member-bound token returned by the preview to EXECUTE. Omit to preview.")
@click.option("--reason", default=None, help="Recorded on every resulting audit row.")
@pass_context
def campaign_action(ctx, campaign_id, action_name, confirm, reason) -> None:
Expand All @@ -677,7 +677,7 @@ def campaign_action(ctx, campaign_id, action_name, confirm, reason) -> None:

\b
Example:
limacharlie mailsec campaign action ec7e273b-... --action quarantine_message --confirm ec7e273b-...
limacharlie mailsec campaign action ec7e273b-... --action quarantine_message --confirm 3c514e...
"""
ms = _get_mailsec(ctx)
_output(ctx, ms.act_on_campaign(campaign_id, action_name, confirm=confirm, reason=reason))
Expand Down
3 changes: 2 additions & 1 deletion limacharlie/sdk/mailsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ def act_on_campaign(
Args:
campaign_id: The campaign to sweep.
action: The typed action, as for :meth:`act_on_message`.
confirm: Pass the campaign id to execute. Omit to preview.
confirm: Pass the member-bound token returned by the preview to
execute. Omit to preview.
reason: Recorded on every resulting audit row.
actor: Ignored if supplied — the gateway stamps the acting
identity from the authenticated claims, so an audit trail's
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/test_cli_mailsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ def invoke_connection(*args: str):
return result, mailsec


def invoke_campaign_action(*args: str):
with (
patch("limacharlie.commands.mailsec.Client"),
patch("limacharlie.commands.mailsec.Organization"),
patch("limacharlie.commands.mailsec.Mailsec") as mailsec_cls,
):
mailsec = MagicMock()
mailsec.act_on_campaign.return_value = {"preview": False, "succeeded": 1}
mailsec_cls.return_value = mailsec
result = CliRunner().invoke(
cli,
[
"--oid", "11111111-2222-3333-4444-555555555555",
"--output", "json",
"mailsec", "campaign", "action", "campaign-1",
*args,
],
)
return result, mailsec


def test_connection_diagnostic_is_read_only_by_default():
result, mailsec = invoke_connection("workspace")
assert result.exit_code == 0, result.output
Expand All @@ -33,3 +54,25 @@ def test_include_watch_reaches_the_public_sdk_call():
result, mailsec = invoke_connection("workspace", "--include-watch")
assert result.exit_code == 0, result.output
mailsec.test_connection.assert_called_once_with("workspace", include_watch=True)


def test_campaign_action_help_requires_the_preview_token():
result = CliRunner().invoke(cli, ["mailsec", "campaign", "action", "--help"])
assert result.exit_code == 0, result.output
assert "member-bound token returned by the preview" in result.output
assert "Pass the campaign id" not in result.output


def test_campaign_action_forwards_the_preview_token_unchanged():
result, mailsec = invoke_campaign_action(
"--action", "quarantine_message",
"--confirm", "member-bound-token",
"--reason", "reviewed current set",
)
assert result.exit_code == 0, result.output
mailsec.act_on_campaign.assert_called_once_with(
"campaign-1",
"quarantine_message",
confirm="member-bound-token",
reason="reviewed current set",
)
4 changes: 2 additions & 2 deletions tests/unit/test_sdk_mailsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ def test_campaign_action_previews_without_confirm(self, ms, mock_org):
assert "confirm" not in body

def test_campaign_action_confirm_is_forwarded(self, ms, mock_org):
ms.act_on_campaign("cmp-1", "quarantine_message", confirm="cmp-1")
ms.act_on_campaign("cmp-1", "quarantine_message", confirm="member-bound-token")
_, body = _post_call(mock_org)
assert body["confirm"] == "cmp-1"
assert body["confirm"] == "member-bound-token"


class TestConnectionDiagnostics:
Expand Down