diff options
| -rw-r--r-- | abusectl/cli.py | 80 |
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: |
