Skip to content

Commit ce9f4c9

Browse files
committed
feat(cli): Add -i / --instruction flag and enhance parameter parsing
1 parent 5792692 commit ce9f4c9

4 files changed

Lines changed: 80 additions & 34 deletions

File tree

README.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,37 @@ secnodeapi --target http://api.local/spec --mode agent --request-budget 500 --ma
115115
```
116116

117117
### Full CLI Options
118-
| Option | Description |
119-
|---|---|
120-
| `--target` | URL or local path to OpenAPI schema (required) |
121-
| `--mode` | `agent` (default) or `legacy` execution pipeline |
122-
| `--concurrency` | Concurrent request workers |
123-
| `--auth-header` | Single inline auth header |
124-
| `--auth-file` | JSON file of auth headers |
125-
| `--identities-file` | JSON identities for differential auth testing |
126-
| `--schema-only` | Output normalized API structure and exit |
127-
| `--dry-run` | Generate tests without executing |
128-
| `--request-budget` | Max request count in agent mode |
129-
| `--max-iterations` | Max plan/execute loops in agent mode |
130-
| `--proxy` | Route traffic via proxy |
131-
| `--insecure` | Disable TLS verification |
118+
### Options
119+
120+
| Flag | Description | Default |
121+
| :--- | :--- | :--- |
122+
| `--target` | URL or local path to OpenAPI schema (required) | `None` |
123+
| `--mode` | `agent` (default) or `legacy` execution pipeline | `agent` |
124+
| `-i`, `--instruction` | Pass parameters (key=value) for AI test generation | `None` |
125+
| `--concurrency` | Number of parallel requests | `5` |
126+
| `--auth-header` | Inline auth header (e.g., `Authorization: Bearer 123`) | `None` |
127+
| `--auth-file` | JSON file of auth headers | `None` |
128+
| `--identities-file` | JSON identities for differential auth testing | `None` |
129+
| `--schema-only` | Output normalized API structure and exit | `False` |
130+
| `--dry-run` | Generate tests without executing | `False` |
131+
| `--request-budget` | Max requests for the entire agent run | `400` |
132+
| `--max-iterations` | Max plan/execute loops in agent mode | `5` |
133+
| `--proxy` | Route traffic through an HTTP proxy | `None` |
134+
| `--insecure` | Disable TLS verification | `False` |
135+
136+
### User Instructions (`-i`)
137+
138+
The `-i` flag allows you to provide the agent with specific data points (tokens, usernames, IDs) to make its adversarial reasoning more realistic. You can use space or comma-separated pairs:
139+
140+
```bash
141+
# Using the -i flag
142+
docker run --rm secnodeapi -i "username=admin token=jasdndsfnfdsng" --target ...
143+
```
144+
145+
The agent will:
146+
1. **Parse** these values during the Buildup Phase.
147+
2. **Inject** them into its AI world-model for understanding the API.
148+
3. **Prioritize** them when generating test cases (e.g., using the provided token for BOLA/BFLA attempts).
132149

133150
## Docker Support
134151

src/secnodeapi/cli.py

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,11 @@ def parse_identities(identities_file: str) -> List[IdentityContext]:
9898

9999

100100
def parse_instructions(args) -> List[InstructionSet]:
101-
"""Parse instruction sets from CLI arguments --instruction1 through --instruction5.
101+
"""Parse instruction sets from CLI arguments -i/--instruction and --instruction1 through --instruction5.
102102
103-
Each argument should be comma-separated key=value pairs, e.g.:
104-
--instruction1 "token=xxx, username=vishn, password=secret123"
103+
Arguments can be comma or space separated key=value pairs, e.g.:
104+
-i "token=xxx username=vishn"
105+
--instruction1 "token=xxx, password=secret123"
105106
106107
Args:
107108
args: argparse Namespace
@@ -111,36 +112,50 @@ def parse_instructions(args) -> List[InstructionSet]:
111112
"""
112113
instructions: List[InstructionSet] = []
113114

115+
# Check new -i/--instruction flag first
116+
main_instr = getattr(args, "instruction", None)
117+
if main_instr and isinstance(main_instr, str):
118+
variables = _parse_kv_string(main_instr)
119+
if variables:
120+
instructions.append(InstructionSet(name="cli_instruction", variables=variables))
121+
122+
# Check legacy numbered flags
114123
for i in range(1, 6):
115124
arg_name = f"instruction{i}"
116125
value = getattr(args, arg_name, None)
117126

118127
if not value or not isinstance(value, str):
119128
continue
120129

121-
# Parse comma-separated key=value pairs
122-
variables: Dict[str, str] = {}
123-
pairs = value.split(",")
124-
125-
for pair in pairs:
126-
pair = pair.strip()
127-
if "=" not in pair:
128-
continue
129-
130-
# Split on first = only, so values can contain =
131-
key, val = pair.split("=", 1)
132-
key = key.strip()
133-
val = val.strip()
134-
135-
if key: # Only add non-empty keys
136-
variables[key] = val
137-
130+
variables = _parse_kv_string(value)
138131
if variables:
139132
instructions.append(InstructionSet(name=arg_name, variables=variables))
140133

141134
return instructions
142135

143136

137+
def _parse_kv_string(value: str) -> Dict[str, str]:
138+
"""Parse a string of key=value pairs separated by commas or spaces."""
139+
variables: Dict[str, str] = {}
140+
141+
# Replace commas with spaces to handle both delimeters uniformly
142+
normalized = value.replace(",", " ")
143+
# Use split() to handle multiple spaces
144+
pairs = normalized.split()
145+
146+
for pair in pairs:
147+
if "=" not in pair:
148+
continue
149+
150+
key, val = pair.split("=", 1)
151+
key = key.strip()
152+
val = val.strip()
153+
154+
if key:
155+
variables[key] = val
156+
return variables
157+
158+
144159
def parse_vars(vars_list: List[str]) -> Dict[str, str]:
145160
"""Parse --var arguments into a dictionary.
146161
@@ -208,6 +223,10 @@ def parse_args():
208223
action="store_true",
209224
help="Generate tests but do not execute",
210225
)
226+
parser.add_argument(
227+
"--instruction", "-i",
228+
help="Main instruction set (key=value pairs, space or comma separated)",
229+
)
211230
parser.add_argument(
212231
"--dry-run-output",
213232
help="Write generated tests to JSON file (use with --dry-run)",

src/secnodeapi/services/pipeline.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,10 @@ async def build_pipeline_artifacts(
378378
pipeline_input: PipelineInput,
379379
) -> tuple:
380380
"""Run schema load, active recon, tool orchestration, understanding, and test generation."""
381+
if pipeline_input.instructions:
382+
for instr in pipeline_input.instructions:
383+
console.print(f"[dim]📝 User instructions loaded ({instr.name}):[/] [cyan]{instr.variables}[/]")
384+
381385
raw_schema = await fetch_schema(
382386
pipeline_input.target,
383387
proxy=pipeline_input.proxy,

src/secnodeapi/services/tool_orchestrator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ async def run_tool_orchestration_phase(
177177

178178
console.rule("[bold blue]Phase 1b: Tool Orchestration")
179179
logger.info("Starting AI-driven tool orchestration phase")
180+
181+
if pipeline_input.instructions:
182+
for instr in pipeline_input.instructions:
183+
console.print(f"[dim]📝 User instructions loaded ({instr.name}):[/] [cyan]{instr.variables}[/]")
184+
185+
api_structure, seed_tests, orch_result = await build_pipeline_artifacts(pipeline_input)
180186

181187
# 1. Get AI tool plan
182188
console.print("[dim]🤖 Asking AI to plan tool invocations...[/]")

0 commit comments

Comments
 (0)