aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-10 16:58:54 +0200
committerDanilo M. <danix@danix.xyz>2026-09-10 16:58:54 +0200
commitfddfef7cd4ff8ec9cac3e3397ff0a07579abf2c9 (patch)
treebd4fc9e48400fb6aa5d02c0ea18bb635dc44239f
parent9a13bd372a79d0c944d06801b1c3c67f8e8b8397 (diff)
downloadabusectl-fddfef7cd4ff8ec9cac3e3397ff0a07579abf2c9.tar.gz
abusectl-fddfef7cd4ff8ec9cac3e3397ff0a07579abf2c9.zip
feat: ask for the reporting destinations at init
The MISP pair is asked and validated TOGETHER, because build() now refuses a half-answered section: a url with no key is configured-and-broken rather than skipped. Skipping the key discards the url and says so, rather than trapping a user who has no key to hand in a repeated question. A double quote is refused at the prompt that asked for it. build() rejects one with a ValueError, and by then every other answer has been given, so a pasted key carrying one would have cost the whole run: the shape of defect the first hand test found four of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0176FYdVfpzUq8S9jecqQqL6
-rw-r--r--abusectl/cli.py80
1 files changed, 79 insertions, 1 deletions
diff --git a/abusectl/cli.py b/abusectl/cli.py
index 636bd4e..800ca1a 100644
--- a/abusectl/cli.py
+++ b/abusectl/cli.py
@@ -256,6 +256,83 @@ def _ask_reporter() -> dict:
return answers
+def _ask_quotable(question: str) -> str:
+ """Ask a skippable free-text answer that must survive `init.build`.
+
+ Validated HERE because build() rejects a quote or a newline with a
+ ValueError, and by then every other answer has been given: a pasted key
+ with a stray quote in it would cost the whole run, which is precisely
+ the shape of defect the first hand test found four of.
+ """
+ while True:
+ answer = _ask(question)
+ if '"' not in answer:
+ return answer
+ print(' A double quote cannot be stored in the config file.')
+ print(" Re-paste it without one, or Enter to skip.")
+
+
+def _ask_destinations() -> dict:
+ """Ask for the reporting destinations, all skippable.
+
+ The MISP pair is validated TOGETHER and here, at the prompt that asked:
+ a url with no key is configured-and-broken rather than skipped, and
+ finding that out at `report` time costs the whole run. That is this
+ repository's standing prompt rule, learned from a hand test that found
+ four instances of one mistake.
+
+ Skippable, and they say so, because the relay question and the hop
+ picker now say they are REQUIRED and a user reads the difference.
+ """
+ print("\nabusectl can also file each case to a threat-intel platform.")
+ print("MISP is your own instance and is written FIRST: it is the record")
+ print("that gates everything irreversible, so a failure there stops the")
+ print("rest. The vendors are public feeds. Leave any blank to skip it.")
+
+ answers = {"misp_url": "", "misp_api_key": ""}
+
+ while True:
+ url = _ask_quotable("\nMISP instance URL (Enter to skip): ")
+ if not url:
+ break
+ # ponytail: the ceiling is deliberately low, the same call
+ # _ask_reporter_email documents. This is not a URL validator and
+ # must not become one: nothing here connects, so the failure a check
+ # can actually prevent is a hostname or an API key pasted into the
+ # wrong prompt, and both lack a scheme. A reachable-looking URL that
+ # is wrong is caught by `submit`, which is the only thing that can
+ # tell.
+ if not url.startswith(("http://", "https://")):
+ print(" Expected a URL such as https://misp.example.org.")
+ continue
+ key = _ask_quotable("MISP API key (Enter to skip MISP entirely): ")
+ if not key:
+ # Half a pair is worse than none: it reads as configured and
+ # fails at submit, and build() refuses to write one at all. The
+ # URL just typed is therefore DISCARDED rather than re-asked:
+ # a user who has no key to hand cannot answer a repeat of the
+ # question, and the line below says what happened so the loss
+ # is not silent. Re-run init once the key exists.
+ print(" A URL with no key cannot be used, so MISP is skipped.")
+ break
+ answers["misp_url"] = url
+ answers["misp_api_key"] = key
+ break
+
+ answers["abusedb_api_key"] = _ask_quotable(
+ "\nAbuseIPDB API key, for reporting IPs (Enter to skip): "
+ )
+ answers["urlhaus_api_key"] = _ask_quotable(
+ "URLhaus Auth-Key, for reporting URLs (Enter to skip): "
+ )
+
+ if not any(answers.values()):
+ print("\n No destinations set. Cases will reach abuse desks only,")
+ print(" which is a complete report; add them later if you want one.")
+
+ return answers
+
+
def _prompt_answers(sample: Path | None) -> dict:
"""Ask the interactive questions and return an answers dict for init.build.
@@ -268,7 +345,8 @@ def _prompt_answers(sample: Path | None) -> dict:
trusted_relays = _ask_relays()
cases = _ask(f"\nCases directory (Enter for {config.DEFAULT_CASES}): ")
- return {"trusted_relays": trusted_relays, "cases": cases, **_ask_reporter()}
+ return {"trusted_relays": trusted_relays, "cases": cases,
+ **_ask_reporter(), **_ask_destinations()}
def _cmd_init(args) -> int: