Skip to content

Development - #159

Merged
andreashappe merged 29 commits into
mainfrom
development
Aug 29, 2026
Merged

Development#159
andreashappe merged 29 commits into
mainfrom
development

Conversation

@andreashappe

Copy link
Copy Markdown
Member
  • remove stale documenation
  • integrated the web-api testcases more with common infrastructure
  • added an active-directory use-case from cochise
  • unified and de-duplicated a lot of code
  • moved priv-esc use-cases into priv_esc

Overall the lines-of-code stayed the same, but we improved readability a bit and added the ActiveDirectory use-case. More cleanups to come.

andreashappe and others added 29 commits August 28, 2026 08:29
The former SimpleWebAPIDocumentation (detection) and SimpleWebAPITesting
(testing) were two separate registered CLI use-cases, even though detection's
entire output is exactly testing's input. Consolidate both into a single
usecases/web_api/ package exposing one registered use-case, WebAPITesting,
with modes document | test | auto (default auto):

  * auto     - detect the surface, then pentest it (or pentest --surface
               directly when given)
  * document - detect and write the OpenAPI spec only
  * test     - pentest a given --surface (an OpenAPI spec or a sitemap)

The two Simple* classes are de-registered (@use_case -> @DataClass) but kept
as internal phase engines the orchestrator and their unit tests construct
directly; WebAPITesting.run() drives each engine's perform_round with its own
per-phase turn budget and never calls their .run().

Integration details:
  * Detection -> testing hand-off with no file round-trip:
    OpenAPISpecificationHandler.to_openapi_document() +
    SimpleWebAPIDocumentation.built_surface_document(), wrapped by
    OpenAPISurface.from_dict. OpenAPISpecificationParser gains an api_data=
    kwarg + from_dict() so specs load from memory.
  * utils/web_api/target_surface.py normalises an OpenAPI spec OR a sitemap
    (URL list / sitemap.xml / HTML) into one surface; a sitemap becomes a
    synthetic OpenAPI api_data dict so the parser works unchanged. Sitemap XML
    is parsed via a regex over <loc> to avoid XXE / billion-laughs; sitemap
    surfaces are schema-less (is_sitemap=True) and degrade to path heuristics.
    SimpleWebAPITesting._injected_surface bypasses the config oas/ loading.
  * Fix a latent path bug in the handler: numeric path segments are now
    replaced whole (/v1/users/1 -> /v1/users/{id}) instead of a bare substring
    replace that corrupted /v1/users and /items/11.

Source files moved via git mv (history preserved); usecases/__init__.py, the
web-api test imports, the README command listing and .gitignore output
patterns updated accordingly. Adds tests/test_target_surface.py and
tests/test_web_api_merged.py (mode routing + surface injection). Full suite:
95 passed, 1 skipped.

The web/ use-case family (web-application pentesting) is unrelated and left
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BRRj9t9uakmTyVQE9t4q6q
The Mac/Codespaces onboarding scripts provisioned a local
ansible-ready-ubuntu container and ran `wintermute LinuxPrivesc`
against it — a use-case name that no longer exists (now PrivEscLinux/
MinimalPrivEscLinux), so the demo was already broken. The current
privesc benchmark (benchmark_privesc.py) drives its own privesc_*
Docker fleet and never referenced these files.

Delete the demo and everything that only supported it:
- MAC.md, CODESPACES.md, .devcontainer/
- scripts/ (all 8 files, incl. Ansible tasks.yaml + hosts.ini)
- .env.example.aws (stale AWS/ssh-key onboarding example)

Clean up now-dangling references:
- README.md: drop the "## Use Cases" section and the .env.example.aws step
- .github/copilot-instructions.md: drop scripts//Ansible/Mac/Codespaces
  mentions; fix stale LinuxPrivesc example -> PrivEscLinux

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N6CzjpEU7X3Xty1ypJZB7m
The testing engine generated PythonTestCase objects and wrote
test_cases.txt + python_test.py artifacts under web_api/tests/, but
nothing ever read, imported, or ran them: they were a write-only
byproduct disconnected from the testing loop, the report, and any
pass/fail logic.

Delete the PythonTestCase capability and the GenerationTestHandler,
drop their wiring from SimpleWebAPITesting (imports, _test_handler,
the python_test_case capability, and the generate_test_cases call in
_handle_response), and remove the generated tests/ output dir. The
write_analysis_to_report call in the same block is preserved, and the
parse/ParsedInformation and record_note capabilities are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SLajve9XECKN3pFrgqmqdg
…tils

Cleanup of utils/web_api + utils/prompt_generation after the WebAPITesting
merge. Behaviour-preserving; full test suite green (94 passed, 1 skipped).

Dead code removed:
- response_analyzer_with_llm: broken __main__ demo, MagicMock import, and
  set_purpose / print_results / replace_account (unused).
- pattern_matcher: __main__ demo.
- openapi_parser: _print_api_details (called a non-existent get_paths),
  get_protected_endpoints, get_refresh_endpoints.
- llm_handler: write-only _get_created_objects accessor (+ its test and the
  stale commented-out test_call_llm block).

Redundant abstractions collapsed:
- endpoint_categorizer.categorize_endpoints_with_query now backs both the
  parser's and the documentation engine's previously-identical
  categorize_endpoints.
- New utils/web_api/http_response.extract_status_code_and_message replaces the
  status-line regex duplicated in the analyzer and the OpenAPI handler.

Detection state machine moved into the use-case:
- New usecases/web_api/detection_response_handler.DetectionResponseHandler holds
  the ExploreStep exploration FSM (handle_response, adjust_path*, get_next_path,
  check_if_successful, common_endpoints, ...). The base utils ResponseHandler is
  now phase-agnostic only (evaluate_result, extract_key_elements_of_response,
  parse_http_status_line, set_response_analyzer). The documentation engine builds
  the subclass; the testing engine keeps the slim base.
- PromptGenerationHelper.get_hint now uses ExploreStep.INSTANCE/SUBRESOURCE/QUERY
  instead of the magic step-numbers 2/3/6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
…TTPRequest

The testing phase drove its own tool-calling loop through LLMHandler so it could
mutate the model's proposed request before executing it, which meant web-API
captured no LLMResult (cost/tokens) and never used structured tool-call logging.

Move that request mutation into a new ProposedHTTPRequest capability whose
__call__ finalises the proposal against the current test step (path override,
POST-on-setup, bearer-token resolution, empty-body fill, path guards), delegates
the send to the real HTTPRequest, then captures response state (adjust_user /
extract_ids). Because mutation now lives inside the capability, the testing round
runs on the standard get_response + CapabilityManager.run_capability_json path:

- get_response gains a tool_choice param (forwarded to raw_completion; ignored
  when no tools are offered) so "force exactly one capability" is preserved via
  tool_choice="required" over a single-capability set.
- the round records the LLMResult (cost/tokens via log.call_response) and executes
  the tool call through run_capability_json (structured log.add_tool_call).
- removed the now-dead bespoke path (adjust_action / execute_response /
  _handle_response) and code only it reached: save_resource / extract_resource_name
  (only the never-taken != "HTTPRequest" branch called them), the unreachable
  write_endpoint_to_report branch, and the uncalled extract_token_from_http_response.

Scope: testing phase only. The detection engine and the response analyzer still
use LLMHandler; removing LLMHandler and its context-window retry is a detection
follow-up. Limits integration remains deferred (web-API uses manual turn loops).

Tests: ProposedHTTPRequest mutation cases, get_response(tool_choice) plumbing, and
test_perform_round re-mocked at the new boundary. Suite: 101 passed, 1 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Finishes moving web-API off its bespoke LLMHandler onto the framework's standard
LiteLLM.get_response path, so detection now captures LLMResult (cost/tokens) and
structured tool-call logging like every other use-case.

The detection FSM must mutate the request before executing it, but that split is
expressible with framework pieces: get_response(tool_choice="required") for the
forced call and capability.tool_call_to_action to rebuild the same un-executed
HTTPRequest action LLMHandler produced. Because that action is exactly what the
ExploreStep FSM, handle_response, document_response and the evaluator already
consume, the detection loop converts with a boundary swap only — no FSM or
consumer re-architecture (the returned is_good was already dead; run_documentation
is steered by the handler's counters).

- simple_openapi_documentation.run_documentation: get_response + call_response +
  tool_call_to_action in place of the LLMHandler call.
- DetectionResponseHandler.handle_response/handle_http_response: take
  message/message_id/tool_call instead of completion; add log.add_tool_call; drop
  the unused categorized_endpoints param and the never-used llm_handler.
- ResponseAnalyzerWithLLM: its one forced call converted the same way; constructor
  now takes llm + capabilities instead of llm_handler.
- Removed dead plumbing: base ResponseHandler / DetectionResponseHandler stored an
  llm_handler they never used; OpenAPISpecificationHandler dropped its llm_handler
  param and 4 _add_created_object calls (no-ops into a reader-less store).
- Deleted utils/web_api/llm_handler.py + tests/test_llm_handler.py; the
  context-window retry is removed with it.

Tests updated to the get_response boundary. Suite: 99 passed, 1 skipped.
git grep for LLMHandler/execute_prompt_with_specific_capability/llm_handler over
src now returns no code hits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
The web-API use-cases run on SimpleStrategy (a max_turns loop) and never used the
framework's Limits, so runs enforced no cost/token/duration budget and couldn't
report why they stopped. Wire Limits through the whole flow:

- WebAPITesting orchestrator: limits.start() in run(); shares one Limits with both
  phase engines (engine.limits = self.limits); both phase loops stop on
  limits.reached() and register_round() per turn; a reached limit ends the run as a
  failure with limits.reason (matching AutonomousUseCase), else success.
- Detection and testing engines: their inner LLM loops also check limits.reached()
  (so one round can't overshoot a token/cost budget), and every get_response calls
  limits.register_message(llm_result) to accumulate tokens + cost.
- ResponseAnalyzerWithLLM: takes a limits param and registers its analysis calls too.
- Safe default: where limits is unset (direct construction / tests), each component
  defaults to a never-reached Limits(0,0,0,0), so existing behaviour is unchanged and
  bounded only by the per-phase turn caps; the CLI injects a real Limits that enforces.

max_rounds now counts rounds across both phases as a global cap, alongside the
existing per-phase detection_max_turns / max_turns caps (kept as secondary bounds).

Tests: mocked-get_response tests give llm_result numeric total_tokens/cost; added
tests that a cost limit exhausted in detection skips testing and is reported as the
failure reason, and that the default limits is never-reached. Suite: 101 passed, 1 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
A readability pass over web_api and its supporting capability/utils code: remove
dead code and degenerate/over-built constructs, behaviour-preserving throughout.

Removed dead components (deleted files):
- SubmitHTTPMethod capability: unreferenced repo-wide (old "find all HTTP methods"
  flow), not exported from capabilities/__init__.
- ParsedInformation capability: constructed in the testing setup but never invoked
  (the analyzer only forces http_request/record_note; the passed capacity was
  write-only). Removed the whole chain: parse_capacity, the dead http_capability,
  the _context test_cases/parsed entries, and the analyzer's capacity param.
- Evaluator: benchmark-only metrics (needs ground-truth config, wrote a .txt every
  detection round, had a latent unbound-variable bug) + its two per-round calls.

Removed dead methods/aliases: found_all_endpoints, write_endpoint_to_report,
SitemapSurface.from_html, a dead nested substitute() in _pentesting_core, and the
no-op `Context = Any` alias (replaced by honest dict annotations).

Simplified PatternMatcher: its four regex patterns and matches_any_pattern/
replace_parameters machinery are provably equivalent to a single
re.sub(r"/\d+", "/{id}", path) in every reachable input (the query-value branch and
the "/1" fallback are unreachable). ~110 lines -> ~17, with a new characterization
test pinning the behaviour.

Deferred (needs characterisation tests first): the 5-way-duplicated prompt-class
transform methods, the side-effecting analyzer parser, and splitting the
mixed-purpose PromptGenerationHelper.

Net -172/+25 across 10 files + 3 deletions. Suite: 104 passed, 1 skipped;
git grep confirms the removed symbols are gone from src.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
…ders

Behaviour-preserving readability refactor of the prompt-generation code, guarded by
characterisation tests written first (seeded golden pentesting prompts for cot/tot/icl)
that stay byte-identical before and after.

Prompt-class dedup:
- Hoist the single _get_pentesting_steps into BasicPrompt, deleting the two ~55-line
  identical copies in StatePlanningPrompt and TaskPlanningPrompt.
- Unify the transform method name: InContextLearningPrompt's
  transform_into_prompt_structure_with_previous_examples -> transform_into_prompt_structure
  (now one abstract on BasicPrompt + three overrides).
- Extract the shared _step_fields(test_case, counter) used by the CoT/ToT/ICL transforms,
  removing ~18 duplicated lines from each (and a stray debug print in ToT).

PromptGenerationHelper cleanup:
- Move the three pure _get_{sub,related,multi_level}_resource_endpoint builders into a new
  utils/web_api/endpoint_shapes.py as functions (dropping the unused `name` param); remove
  them from the helper and update the detection-handler call sites.

Tests: tests/test_prompt_characterization.py (the golden safety net) and
tests/test_endpoint_shapes.py. Net -222 lines across 8 files + 3 new files.
Suite: 110 passed, 1 skipped. git grep confirms one _get_pentesting_steps definition and
no remaining duplicate transform/builder names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
parse_http_response tangled HTTP parsing (status/headers/body, JSON decode, HTML
stripping) with stateful account/token capture and three stray debug prints.

Faithful, byte-identical factoring: the method is now a readable parse skeleton that
calls four named, guarded side-effect helpers extracted from the inline branches:
- _capture_token(body): token -> analyzer/current_user/matching account
- _capture_ids(body): matching-value id -> account
- _note_account_from_list(body): list-branch capture
- _note_current_account(): fallback account registration
Each returns early when its guard fails, so the skeleton calls them unconditionally,
matching the old inline behaviour exactly. Both callers are unchanged and still fire
the capture, so behaviour is byte-identical.

Also removed the three debug prints, replaced body.__contains__(x) with x in body, and
dropped the always-true (body != '' or body != "") tautology.

Characterisation tests written first (previously the side effects had no coverage):
token capture and id capture with a real prompt_helper, plus the existing parse-return
tests. All stay green unchanged through the refactor. Suite: 112 passed, 1 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
The generic OWASP web/API security knowledge (auth, authz, injection, session,
rate-limiting, misconfig, logging, ...) was locked inside web-api's scripted,
OpenAPI-bound scenario generators, while the separate `web` agents had no structured
coverage at all. Extract that knowledge into an agent-agnostic form and expose it as a
capability both agents can use.

- utils/pentest_playbook.py: the generic knowledge as data (topic -> checklist of what
  to test / verify), no target or endpoint binding; case/separator-insensitive lookup.
- capabilities/pentest_playbook.py: PentestPlaybook, an LLM-callable knowledge lookup
  modelled on RecordNote (describe() + async __call__(topic)); pure, no side effects.
- Wire PentestPlaybook into the direct web agents (with_explanation, with_shell), which
  previously had no structured OWASP checklist to consult.

For web-api the same module is usable as a data source (its testing round forces a
single tool via tool_choice, so a callable capability is not reachable there).

Tests: tests/test_pentest_playbook.py (topics, lookup, capability, describe). Suite:
117 passed, 1 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
…tion tests

get_path_and_schema crashed on a login endpoint with no schema ("argument of type
'NoneType' is not iterable"), even though its callers already handle a None schema
(they skip, or pass it to helpers that guard None). Return (path, None) instead of
crashing. This is a genuine bug fix and it unblocks the SESSION_MANAGEMENT generator on
the test fixture.

Add tests/test_pentesting_scenarios.py: seeded (Faker/random + pinned secrets.token_hex)
golden md5s of explore_steps(purpose) for the purposes that produce non-empty
deterministic output on the fakeapi fixture (authorization, input-validation,
error-handling, session, xss, business-logic, misconfiguration) — the safety net for the
upcoming scenario-file cleanup. Suite: 117 passed + 7 subtests, 1 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Introduce PenTestingCore._test_case(objective, *, steps, path, token,
expected_response_code, security) which builds one scenario test case and documents in
one place the parallel, index-aligned list contract the prompt transforms rely on
(previously implicit in ~82 inline dicts). Convert _pentesting_session.py to use it and
drop its redundant "# This prompt tests..." comments (234 -> 212 lines).

Behaviour-preserving: the SESSION_MANAGEMENT scenario golden
(tests/test_pentesting_scenarios.py) is unchanged. Suite: 118 passed + 7 subtests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Convert the 10 scenario dicts in _pentesting_misc.py (error-handling, misconfiguration,
logging) to PenTestingCore._test_case(...) and drop the redundant per-dict comments
(359 -> 316 lines). Behaviour-preserving: the ERROR_HANDLING / SECURITY_MISCONFIGURATIONS
scenario goldens are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Replace the remaining inline test-case dict literals in _pentesting_authz.py
with self._test_case(...) calls and drop the redundant duplicated comment
block. Behaviour-preserving: the AUTHORIZATION golden (test_pentesting_scenarios)
is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Replace the 26 inline test-case dict literals in _pentesting_injection.py
with self._test_case(...) calls and drop the redundant per-dict
'# This prompt/request tests...' comments. Behaviour-preserving: the
INPUT_VALIDATION / CROSS_SITE_SCRIPTING / BUSINESS_LOGIC_VULNERABILITIES
goldens (test_pentesting_scenarios) are unchanged. The pre-existing
malformed 'security' set literal in test_css (dead on the fixture) is
preserved verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Replace the 5 inline test-case dict literals in _pentesting_ratelimit.py
with self._test_case(...) calls and drop the redundant per-dict comments.
This file's scenarios are not exercised by the fixture golden (RATE_LIMITING
raises on fakeapi), so the conversion was verified byte-identical with an
AST-equivalence check: each converted call reconstructs the same 6 keys from
the same value expressions (ast.dump match) as the original dict literal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Replace the 16 inline test-case dict literals in _pentesting_auth.py with
self._test_case(...) calls and drop the redundant per-dict explanatory
comments. This file's scenarios are not exercised by the fixture golden
(AUTHENTICATION yields no cases on fakeapi), so the conversion was verified
byte-identical with an AST-equivalence check (each converted call
reconstructs the same 6 keys from the same value expressions). Pre-existing
latent bugs on dead paths (e.g. random.sample without a count) are preserved
verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Convert the remaining 8 inline test-case dict literals in _pentesting_core.py
(verify_setup, resource_prompts, resource_endpoints, generate_user) to
self._test_case(...) calls and drop the redundant explanatory comment block.
The _test_case builder's own return dict is left untouched. With this all 82
scenario test-cases across the _pentesting_* mixins now go through the single
builder (0 raw dict literals remain). Verified byte-identical: the golden
suite is unchanged and an AST-equivalence check confirms each converted call
reconstructs the original dict's keys/value-expressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
…ication

Pin the exact behaviour of the shared native-tool-calling round (assistant
message then tool results appended in order, one register_message +
register_round, execution via run_capability_json) that the web agents and
MinimalToolCallPrivEscLinux rely on, so the upcoming CapabilityRegistry and
run_tool_calling_turn refactors can prove they preserve it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Extract the duplicated add_capability / get_capability / run_capability_json /
run_capability_simple_text / get_capability_block logic into a single
CapabilityRegistry mixin (capability.py). Agent and CapabilityManager now both
inherit it, so capability management has one implementation shared by every
use-case family: the web agents and MinimalToolCallPrivEscLinux (Agent), and
the CommandStrategy priv-esc use-cases and the SimpleStrategy web-api engines
(CapabilityManager). No use-case files change - they keep calling the same
methods.

Also fixes CapabilityManager's class-level mutable-default (_capabilities/
_default_capability shared across instances) by storing them per instance, and
gives Agent the run_capability_simple_text that TemplatedAgent already calls.

Behaviour-preserving: the ChatAgent characterization and the full suite stay
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Add run_tool_calling_turn() and route both the ChatAgent loop and the web-api
testing engine through it, removing the hand-rolled duplicate. The helper owns
one LLM tool-calling turn: ask the model, log/register the result, append the
assistant message, then execute each tool call via run_capability_json and
append a tool message. Parameters cover the caller differences - offered
capabilities, tool_choice, sequential vs concurrent execution, an optional
result transform, and an optional per-call reporting hook.

- ChatAgent.perform_round (web agents + MinimalToolCallPrivEscLinux) delegates
  to it; the old Agent.run_tool_calls is folded in and removed. Byte-identical:
  the ChatAgent characterization stays green, and the default call omits
  tool_choice exactly as before.
- SimpleWebAPITesting._run_testing_step becomes one call with tool_choice=
  required, sequential=True, a response-condensing transform, and a new
  _analyse_and_report hook (per-call vulnerability + LLM analysis). The engine
  test stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
The scenario generators inlined the same generic security-check and
expected-response-code sentences verbatim many times (an access-control note
7x, the brute-force block 3x, etc.). Move those 14 full generic strings into
named constants in utils/pentest_playbook.py (a GENERIC_NOTES section beside
the topic checklists) and reference them from the _pentesting_* scenario files,
so the generic knowledge has one home and the copies are gone.

Byte-identical: each constant was verified to equal the original literal
exactly (AST-decoded), all occurrences were replaced (counts checked), and the
scenario goldens stay green. The golden-unguarded files (auth) are covered by
the exact-value proof.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
The only test exercising this loop (integration_minimal_test.py) is skipped -
it imports a pre-PR-#141 module layout (usecases.examples.agent /
usecases.privesc.linux) that no longer exists. Add a live characterization that
drives the real PrivEscLinux and MinimalPrivEscLinux through a scripted
enumerate -> escalate -> root sequence via a fake SSH connection + fake LLM,
pinning that run({}) returns True on root and False when the turn budget is
exhausted. Guards CommandStrategy.run before it is converged onto the shared
AutonomousUseCase.run + Limits loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Neither has any live reference - the only importer of their conceptual
ancestors is tests/integration_minimal_test.py, which is skipped (it targets a
pre-PR-#141 module layout that no longer exists). Drop the now-unused mako
Template and log_conversation imports with them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Collapse the duplicate run loops onto AutonomousUseCase.run so every leaf
use-case shares one Limits-driven loop:

- AutonomousUseCase.run now returns success (limits.reason is None) so
  orchestrators like call_usecase_from_usecase can consume the outcome; callers
  that ignore the return are unaffected.
- CommandStrategy is based on AutonomousUseCase; its bespoke max_turns run loop
  is removed. perform_round() (no turn) runs the round, signals success via
  limits.complete() on check_success, and calls limits.register_round(); the
  capability block is exposed to the template in before_run(). init() seeds a
  Limits and folds max_turns into the round cap (strictest of max_turns and any
  injected --max_rounds wins); get_next_command registers the round's LLM call
  so the cost/token/duration caps now also bound strategy runs (uniform Limits).
- SimpleStrategy's vestigial max_turns loop (never reached - WebAPITesting
  overrides run(); the engines are driven externally) is replaced by an explicit
  NotImplementedError stub; the dead _got_root is dropped.

The linux/windows/minimal privesc use-cases inherit CommandStrategy.perform_round
and need no changes. Behaviour-preserving: the strategy characterization
(run-to-root True / budget-exhausted False) and the full suite stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLA8kcbLZvM3gwwvPCQEjW
Ports the attack tool from the cochise research prototype into hackingBuddyGPT
as a new `AD` (Active Directory assumed-breach) use-case, reusing existing
infrastructure and adding no new dependencies. The log replayer and analysis
tools are intentionally not imported.

New package src/hackingBuddyGPT/usecases/ad/:
- ad.py: ADPlanner (persistent strategic agent) + ADUseCase (@use_case, CLI
  name `AD`). Seeds cochise's 4-message plan history, delegates via perform_task,
  ends early via a new objective_complete tool, optional history compaction.
- executor.py: ADExecutor (ephemeral per-task worker) + PerformTaskCapability,
  modelled on SubAgentCapability: carves parent_limits.sub_limit, runs the worker
  with the SSH tool + a local Knowledge + a complete tool, and merges the worker's
  dirty knowledge back into the planner's global Knowledge.
- knowledge.py: near-verbatim port of cochise's dirty-flag Knowledge.
- templates/: scenario/planner prompts copied verbatim; the executor prompt is
  rendered with stdlib string.Template (Mako's `##` comment would eat markdown
  headers).

Shared infra reused by the worker's SSH tool:
- utils/connectors/async_ssh_connection.py: AsyncSSHConnection (asyncssh, one
  channel per command for genuine parallel execution, 600s timeout, lazy connect).
- capabilities/ssh_execute_command.py: SSHExecuteCommand (tool name
  execute_command); registered in capabilities/__init__.py.

Reused rather than re-implemented: the shared Limits run loop, ChatAgent /
run_tool_calling_turn, LiteLLM.get_response, function_call_capability /
Capability.to_model (so numpydoc is not needed), JsonlLogger, and the
@configurable/parameter config system. The scenario is a swappable file via
--scenario_path (bundled AD default) rather than hardcoded, keeping target scope
config-driven.

Tests: tests/test_ad.py (Knowledge merge/resolve, registration, perform_task
happy-path + forced-summary fallback). Full suite 128 passed / 1 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoAhn2jzM1Leq9X4MjSiet
…riv-esc

Extract shared bases across the capability and use-case families, remove dead
code, and make the Windows priv-esc use-case live and actually functional.

capabilities:
- add SSHCommandCapability base for the two execute_bash_command capabilities
  (shared fields, tool name, prefix-stripping, describe/banner tail)
- add TestCredentialCapability base (shared test_credential name + auth-error helper)
- fix PSExecRunCommand.describe (@Property -> method) and unify the base-import style
- PSExecTestCredential now verifies the logged-in identity (whoami /groups) and only
  reports root success for a genuinely elevated token, instead of always claiming admin
- remove the YAMLFile no-op capability (its __call__ body was commented out and it was
  only stored in a write-only handler attribute)

usecases:
- add WebTestingAgent base for the three web agents (flag params, shared prompt
  fragments, SubmitFlag/EndRun wiring); emitted system prompts stay byte-identical
- add TemplatedCommandPrivEsc base + hoisted template for the minimal Linux/Windows
  strategy priv-esc use-cases
- register PrivEscWindows (was decorated but never imported, so unreachable) and give it
  a working check_success

utils:
- shell_root_detection: add is_admin_from_whoami / check_windows_admin_success

tests:
- add Windows root-detection and PSExecTestCredential coverage
- drop the permanently-skipped integration_minimal_test.py (pre-PR#141 layout)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJRtekD3nmE6yqdTe77Bz
Move the privilege-escalation modules out of the flat usecases/ namespace
into a dedicated priv_esc/ subpackage, matching the existing web/, web_api/
and ad/ convention. Pure relocation + import rewiring, no behavior change:
use-cases register by class name via @use_case, so CLI names, benchmark
aliases and packaging entry points are unaffected.

- move linux_privesc, minimal_linux_privesc,
  minimal_linux_privesc_tool_calling and windows_privesc into priv_esc/
- rename the shared base _privesc_common.py -> priv_esc/_base.py
  (matches web/_base.py); subclasses import it relatively
- add priv_esc/__init__.py re-exporting the four modules so their
  @use_case decorators still run on package import
- collapse the four priv-esc star-imports in usecases/__init__.py into
  `from .priv_esc import *`
- keep call_usecase_from_usecase.py in place, repoint its PrivEscLinux import
- update the two priv-esc test modules to the new import paths

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RezLqdd2GNRMXJxv2jD2cy
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@andreashappe
andreashappe merged commit a8bd3e6 into main Aug 29, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant