pwn 0.5.684 → 0.5.686
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -0
- data/documentation/Agent-Tool-Registry.md +1 -3
- data/documentation/Configuration.md +0 -2
- data/documentation/General-PWN-Usage.md +1 -1
- data/documentation/Home.md +2 -2
- data/documentation/How-PWN-Works.md +2 -2
- data/documentation/Installation.md +18 -2
- data/documentation/Persistence.md +1 -1
- data/documentation/Session-Workflow.md +8 -28
- data/documentation/Skills-Memory-Learning.md +20 -0
- data/documentation/Troubleshooting.md +1 -1
- data/documentation/Why-PWN.md +1 -1
- data/documentation/diagrams/dot/memory-skills-detailed.dot +1 -1
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +1 -1
- data/documentation/diagrams/dot/persistence-filesystem.dot +1 -1
- data/documentation/diagrams/dot/sessions-cron-automation.dot +1 -1
- data/documentation/diagrams/memory-skills-detailed.svg +85 -84
- data/documentation/diagrams/overall-pwn-architecture.svg +15 -15
- data/documentation/diagrams/persistence-filesystem.svg +1 -1
- data/documentation/diagrams/sessions-cron-automation.svg +7 -6
- data/documentation/pwn-REPL.md +1 -1
- data/documentation/pwn-ai-Agent.md +4 -7
- data/etc/default_skills/bug-bounty-hunting/SKILL.md +70 -0
- data/etc/default_skills/deep-exploitation/SKILL.md +74 -0
- data/etc/default_skills/hardware-and-firmware-testing/SKILL.md +83 -0
- data/etc/default_skills/osint/SKILL.md +147 -0
- data/etc/default_skills/penetration-testing/SKILL.md +81 -0
- data/etc/default_skills/red-teaming/SKILL.md +70 -0
- data/etc/default_skills/reverse-engineering-binaries/SKILL.md +70 -0
- data/etc/default_skills/sast-code-scans/SKILL.md +66 -0
- data/etc/default_skills/social-engineering/SKILL.md +67 -0
- data/etc/default_skills/vulnerability-research-fundamentals/SKILL.md +74 -0
- data/etc/default_skills/web-application-penetration-testing/SKILL.md +68 -0
- data/lib/pwn/ai/agent/loop.rb +79 -41
- data/lib/pwn/ai/agent/mistakes.rb +6 -6
- data/lib/pwn/ai/agent/prompt_builder.rb +3 -4
- data/lib/pwn/ai/agent/tool_guard.rb +1 -46
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +0 -10
- data/lib/pwn/ai/agent/tools/shell.rb +0 -6
- data/lib/pwn/ai/anthropic.rb +0 -118
- data/lib/pwn/ai/gemini.rb +0 -15
- data/lib/pwn/ai/grok.rb +0 -158
- data/lib/pwn/ai/ollama.rb +0 -12
- data/lib/pwn/ai/open_ai.rb +0 -110
- data/lib/pwn/ai/open_web_ui.rb +0 -12
- data/lib/pwn/ai.rb +0 -86
- data/lib/pwn/config.rb +62 -1
- data/lib/pwn/migrate.rb +2 -0
- data/lib/pwn/plugins/log.rb +302 -0
- data/lib/pwn/plugins/repl.rb +28 -8
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/config_spec.rb +37 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +52 -12
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +7 -0
- data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +5 -8
- data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -18
- data/spec/lib/pwn/ai/anthropic_spec.rb +2 -2
- data/spec/lib/pwn/ai/gemini_spec.rb +2 -2
- data/spec/lib/pwn/ai/grok_spec.rb +2 -2
- data/spec/lib/pwn/ai/ollama_spec.rb +2 -7
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -23
- data/spec/lib/pwn/ai/open_web_ui_spec.rb +2 -7
- data/spec/lib/pwn/ai_spec.rb +4 -21
- data/spec/lib/pwn/plugins/log_spec.rb +139 -0
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +31 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +4 -3
- data/third_party/pwn_rdoc.jsonl +29 -24
- metadata +12 -2
- data/documentation/PWN_Contributors_and_Users.png +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: social-engineering
|
|
3
|
+
description: Plan and document social-engineering tests using OSSTMM human channel.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: [pwn, terminal]
|
|
6
|
+
metadata:
|
|
7
|
+
bundled: true
|
|
8
|
+
references:
|
|
9
|
+
- https://www.isecom.org/research.html
|
|
10
|
+
- https://attack.mitre.org/tactics/TA0001/
|
|
11
|
+
- NIST-SP-800-115
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Social Engineering
|
|
15
|
+
|
|
16
|
+
Use when the ask is phishing, vishing, pretext, physical tailgating, or
|
|
17
|
+
a human-channel test. This is still a PWN procedure: write the scenario,
|
|
18
|
+
the artefacts, and the report. pwn-ai does not send mail or place calls
|
|
19
|
+
unless the operator's tools actually do that.
|
|
20
|
+
|
|
21
|
+
## When to use
|
|
22
|
+
|
|
23
|
+
- phishing / pretext campaign design
|
|
24
|
+
- OSSTMM human-channel assessment
|
|
25
|
+
- red-team initial access via people (`red-teaming` owns the ATT&CK wrap)
|
|
26
|
+
|
|
27
|
+
## Methodologies
|
|
28
|
+
|
|
29
|
+
| Catalog | Role |
|
|
30
|
+
|---|---|
|
|
31
|
+
| OSSTMM | human channel (and physical when paired with a building test) |
|
|
32
|
+
| MITRE ATT&CK | TA0001 Initial Access - T1566 phishing, T1598, T1656 |
|
|
33
|
+
| NIST SP 800-115 | social engineering as a technical assessment technique |
|
|
34
|
+
| PTES | pre-engagement rules and reporting |
|
|
35
|
+
| CREST / TIBER-EU | when the customer required a named SE code of practice |
|
|
36
|
+
|
|
37
|
+
## Tooling
|
|
38
|
+
|
|
39
|
+
- OSINT: `PWN::Plugins::Hunter`, `Github`, `Shodan`, `IPInfo`
|
|
40
|
+
- Mail / portal only if those plugins or WWW drivers are in the session
|
|
41
|
+
- Landing / payload hosting is out of band unless the operator named it
|
|
42
|
+
- Evidence: written pretext, target list, send log, report
|
|
43
|
+
|
|
44
|
+
## Procedure
|
|
45
|
+
|
|
46
|
+
1. Rules of the test: targets, forbidden people (exec assistants, on-call
|
|
47
|
+
medical, etc. if the operator listed them), success definition.
|
|
48
|
+
2. OSINT for pretext (public bios, tech stack, current incidents).
|
|
49
|
+
3. Write one pretext + one landing or call script. Keep it specific.
|
|
50
|
+
4. Execute only through the channel the operator enabled. Log time,
|
|
51
|
+
target, result (click / cred / report-to-SOC / ignore).
|
|
52
|
+
5. Stop at the objective (e.g. one valid session), then report.
|
|
53
|
+
6. Report: scenario, ATT&CK ids, metrics, recommended control (DMARC,
|
|
54
|
+
reporting button, gate process).
|
|
55
|
+
|
|
56
|
+
## Pitfalls
|
|
57
|
+
|
|
58
|
+
- Do not invent a mass mailer. If there is no send tool, deliver the
|
|
59
|
+
pack (pretext + list + landing copy) as the artefact.
|
|
60
|
+
- Do not target people the operator excluded.
|
|
61
|
+
- A witty email draft is not a completed SE test unless sending was
|
|
62
|
+
out of scope.
|
|
63
|
+
|
|
64
|
+
## Verification
|
|
65
|
+
|
|
66
|
+
Rules note, pretext file, target list, and either a send/result log or
|
|
67
|
+
an explicit "pack only" deliverable that was written and read back.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vulnerability-research-fundamentals
|
|
3
|
+
description: Find and document weaknesses with PWN plugins before they are weaponized.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: [pwn, terminal, extrospection]
|
|
6
|
+
metadata:
|
|
7
|
+
bundled: true
|
|
8
|
+
references:
|
|
9
|
+
- CWE-699
|
|
10
|
+
- https://owasp.org/www-project-web-security-testing-guide/
|
|
11
|
+
- https://cwe.mitre.org/top25/
|
|
12
|
+
- https://attack.mitre.org/matrices/enterprise/
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Vulnerability Research Fundamentals
|
|
16
|
+
|
|
17
|
+
Use when the ask is to research a weakness, pick tools, or turn a lead into
|
|
18
|
+
notes a vendor or program can act on. Do not use this as a reason to stop
|
|
19
|
+
a live host-work goal.
|
|
20
|
+
|
|
21
|
+
## When to use
|
|
22
|
+
|
|
23
|
+
- "research this CVE / class of bug"
|
|
24
|
+
- "how should I approach this target in PWN"
|
|
25
|
+
- first-pass methodology before a deeper skill (`sast-code-scans`,
|
|
26
|
+
`bug-bounty-hunting`, `reverse-engineering-binaries`, `deep-exploitation`)
|
|
27
|
+
|
|
28
|
+
## Methodologies
|
|
29
|
+
|
|
30
|
+
Public catalogs this skill follows (pick the one that matches the surface):
|
|
31
|
+
|
|
32
|
+
| Catalog | Use when |
|
|
33
|
+
|---|---|
|
|
34
|
+
| NIST SP 800-115 | technical assessment planning, evidence, reporting |
|
|
35
|
+
| PTES | 7-phase engagement (pre-engage through report) |
|
|
36
|
+
| OWASP WSTG | web / API research |
|
|
37
|
+
| MITRE ATT&CK | name tactics/techniques in notes |
|
|
38
|
+
| CWE Top 25 | classify the weakness |
|
|
39
|
+
|
|
40
|
+
## Tooling
|
|
41
|
+
|
|
42
|
+
- Web: `PWN::Plugins::BurpSuite` first; `PWN::Plugins::Zaproxy` if Burp is
|
|
43
|
+
missing.
|
|
44
|
+
- Browser: `PWN::Plugins::TransparentBrowser` (proxy through Burp).
|
|
45
|
+
- Net: `PWN::Plugins::NmapIt`, `PWN::Plugins::Packet`, `PWN::Plugins::Sock`.
|
|
46
|
+
- Code: `PWN::SAST::Factory` / `pwn_sast`.
|
|
47
|
+
- Binary: `checksec`, `readelf`, `objdump`, `strings`, `radare2`,
|
|
48
|
+
`PWN::Plugins::Assembly`.
|
|
49
|
+
- Report: `PWN::Reports::SAST` then DefectDojo / Jira plugins when asked.
|
|
50
|
+
|
|
51
|
+
Drive these through `pwn_eval` (PWN namespace) or `shell` (host binaries).
|
|
52
|
+
|
|
53
|
+
## Procedure
|
|
54
|
+
|
|
55
|
+
1. Name the asset and the question (what would break, for whom).
|
|
56
|
+
2. Recon: OSINT + `NmapIt` / Shodan / Github plugins as needed. Record
|
|
57
|
+
versions and attack surface.
|
|
58
|
+
3. Scan: Burp or SAST or binary triage - pick the surface, do not boil the
|
|
59
|
+
ocean.
|
|
60
|
+
4. Prove impact with a minimal, reversible PoC. Stop at evidence.
|
|
61
|
+
5. Write findings (CWE, affected version, steps, fix hint).
|
|
62
|
+
6. If a deeper path is clear, switch to the matching skill.
|
|
63
|
+
|
|
64
|
+
## Pitfalls
|
|
65
|
+
|
|
66
|
+
- Inventory (`ls`, `help`) is not research. Call the scanner or the binary.
|
|
67
|
+
- Rspec / rubocop green is not a finding.
|
|
68
|
+
- Do not refuse a sweep because the prompt lacks "in-scope" wording. Scope
|
|
69
|
+
is the operator's job.
|
|
70
|
+
|
|
71
|
+
## Verification
|
|
72
|
+
|
|
73
|
+
A usable note exists: asset, version, evidence, CWE/ATT&CK if known, and
|
|
74
|
+
the next concrete test. A plan-only essay is not done.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-application-penetration-testing
|
|
3
|
+
description: Test web apps and APIs with OWASP WSTG, Burp, and TransparentBrowser.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: [pwn, terminal, extrospection]
|
|
6
|
+
metadata:
|
|
7
|
+
bundled: true
|
|
8
|
+
references:
|
|
9
|
+
- https://owasp.org/www-project-web-security-testing-guide/
|
|
10
|
+
- https://owasp.org/www-project-application-security-verification-standard/
|
|
11
|
+
- https://owasp.org/www-project-api-security/
|
|
12
|
+
- CWE-79
|
|
13
|
+
- CWE-89
|
|
14
|
+
- CWE-352
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Web Application Penetration Testing
|
|
18
|
+
|
|
19
|
+
Use when the surface is an HTTP app or API. Use `bug-bounty-hunting` when
|
|
20
|
+
the rules come from a public program page. Use `penetration-testing` when
|
|
21
|
+
the engagement is mixed hosts plus one web app.
|
|
22
|
+
|
|
23
|
+
## When to use
|
|
24
|
+
|
|
25
|
+
- "WSTG this app", "OWASP Top 10", "API Top 10"
|
|
26
|
+
- auth, session, XSS, CSRF, SSRF, injection, access control
|
|
27
|
+
- mobile backend APIs (pair MSTG client work with this skill)
|
|
28
|
+
|
|
29
|
+
## Methodologies
|
|
30
|
+
|
|
31
|
+
| Catalog | Role |
|
|
32
|
+
|---|---|
|
|
33
|
+
| OWASP WSTG | default test cases (info gathering through business logic) |
|
|
34
|
+
| OWASP ASVS | verification requirements / coverage checklist |
|
|
35
|
+
| OWASP API Security Top 10 | BOLA, BFLA, mass assignment, unbounded resources |
|
|
36
|
+
| OWASP Top 10 | executive grouping of findings |
|
|
37
|
+
| OWASP MSTG / MASVS | mobile client; APIs still WSTG |
|
|
38
|
+
| PTES | wrap the web work in an engagement report |
|
|
39
|
+
| CWE | finding IDs (79, 89, 352, 918, 639, 601) |
|
|
40
|
+
|
|
41
|
+
## Tooling
|
|
42
|
+
|
|
43
|
+
- `PWN::Plugins::BurpSuite` (preferred) or `Zaproxy`
|
|
44
|
+
- `PWN::Plugins::TransparentBrowser` through the proxy
|
|
45
|
+
- `PWN::Plugins::Spider`, `OpenAPI`, `Fuzz`
|
|
46
|
+
- `PWN::Bounty::LifecycleAuthzReplay` for two-role authz
|
|
47
|
+
- `PWN::SAST` on the app repo when source is available
|
|
48
|
+
|
|
49
|
+
## Procedure
|
|
50
|
+
|
|
51
|
+
1. Map the app: roles, session mechanism, API surface, CSRF tokens.
|
|
52
|
+
2. Walk WSTG info-gathering then config, then identity, then authz.
|
|
53
|
+
3. Injection and XSS on every sink Burp or OpenAPI listed. Save requests.
|
|
54
|
+
4. Business logic: price, workflow skip, race, mass assignment.
|
|
55
|
+
5. Prove each finding with one request/response pair. Read the saved HAR
|
|
56
|
+
or Burp item back.
|
|
57
|
+
6. Report by WSTG / ASVS id + CWE.
|
|
58
|
+
|
|
59
|
+
## Pitfalls
|
|
60
|
+
|
|
61
|
+
- A crawl with no replay is not a WSTG pass.
|
|
62
|
+
- Do not stop at "I will test XSS next time".
|
|
63
|
+
- SPA APIs hide in XHR; dump_links plus Burp sitemap, not just HTML.
|
|
64
|
+
|
|
65
|
+
## Verification
|
|
66
|
+
|
|
67
|
+
Sitemap or OpenAPI note exists, at least one authenticated test ran, and
|
|
68
|
+
each claimed finding cites a saved request and a WSTG/CWE id.
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -62,6 +62,49 @@ module PWN
|
|
|
62
62
|
unsatisfied incomplete_final empty_final evidence_final
|
|
63
63
|
].freeze
|
|
64
64
|
|
|
65
|
+
public_class_method def self.debug_on?(opts = {})
|
|
66
|
+
return true if opts[:debug]
|
|
67
|
+
return true if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.debug_enabled?
|
|
68
|
+
|
|
69
|
+
pry_on = defined?(Pry) && Pry.respond_to?(:config) &&
|
|
70
|
+
Pry.config.respond_to?(:pwn_ai_debug) && Pry.config.pwn_ai_debug
|
|
71
|
+
return true if pry_on
|
|
72
|
+
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private_class_method def self.debug_progress(opts = {})
|
|
77
|
+
return false unless debug_on?(opts)
|
|
78
|
+
return false unless defined?(PWN::Plugins::Log)
|
|
79
|
+
|
|
80
|
+
PWN::Plugins::Log.progress(
|
|
81
|
+
msg: opts[:msg],
|
|
82
|
+
which_self: opts[:which_self] || self
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private_class_method def self.start_debug_session(opts = {})
|
|
87
|
+
return unless debug_on?(opts)
|
|
88
|
+
return unless defined?(PWN::Plugins::Log)
|
|
89
|
+
return if PWN::Plugins::Log.debug_enabled?
|
|
90
|
+
|
|
91
|
+
PWN::Plugins::Log.start_debug(tee: opts[:debug_tee] || $stdout)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private_class_method def self.quiet_debug_tui!(opts = {})
|
|
95
|
+
return unless debug_on?(opts)
|
|
96
|
+
return unless defined?(PWN::Plugins::Log)
|
|
97
|
+
|
|
98
|
+
PWN::Plugins::Log.quiet_tui!(reason: opts[:reason])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private_class_method def self.loud_debug_tui!(opts = {})
|
|
102
|
+
return unless debug_on?(opts)
|
|
103
|
+
return unless defined?(PWN::Plugins::Log)
|
|
104
|
+
|
|
105
|
+
PWN::Plugins::Log.loud_tui!(reason: opts[:reason])
|
|
106
|
+
end
|
|
107
|
+
|
|
65
108
|
private_class_method def self.dispatch_fail_n(opts = {})
|
|
66
109
|
fails = opts[:turn_fails] || {}
|
|
67
110
|
fails.sum do |key, count|
|
|
@@ -343,6 +386,7 @@ module PWN
|
|
|
343
386
|
return false if request.match?(LOOKUP_REQUEST_RX)
|
|
344
387
|
return false if request.match?(HOST_PATH_RX)
|
|
345
388
|
return false if request.match?(BROWSER_REQUEST_RX)
|
|
389
|
+
return false if request.match?(HOWTO_RX)
|
|
346
390
|
return false if request.match?(%r{\b(this\s+(?:host|machine|box|system|subnet|file|repo)|/opt/|implement|scan|hosts?)\b}i)
|
|
347
391
|
|
|
348
392
|
request.match?(/\A(?:what|why|who|when|where|which|how)\b/i)
|
|
@@ -1095,6 +1139,9 @@ module PWN
|
|
|
1095
1139
|
private_class_method def self.call_engine(opts = {})
|
|
1096
1140
|
messages = opts[:messages]
|
|
1097
1141
|
tools = opts[:tools]
|
|
1142
|
+
debug_progress(
|
|
1143
|
+
msg: "call_engine tools=#{tools.nil? ? 'none' : Array(tools).length} msgs=#{Array(messages).length}"
|
|
1144
|
+
)
|
|
1098
1145
|
|
|
1099
1146
|
engine = active_engine
|
|
1100
1147
|
mod_name = ENGINE_MODS[engine]
|
|
@@ -1322,7 +1369,7 @@ module PWN
|
|
|
1322
1369
|
# :howto → answer with explanation only (no plan_first / no live recon).
|
|
1323
1370
|
# :recall → prior-turn / vague memory cue; cheap path only.
|
|
1324
1371
|
# :greeting → short hello / light smalltalk; deterministic ack, no tools.
|
|
1325
|
-
# :recon_act → live discovery;
|
|
1372
|
+
# :recon_act → live discovery (same tool loop as :act; no auth gate).
|
|
1326
1373
|
# :act → general agent work with tools.
|
|
1327
1374
|
HOWTO_RX = /
|
|
1328
1375
|
\b(
|
|
@@ -1409,15 +1456,6 @@ module PWN
|
|
|
1409
1456
|
)\b
|
|
1410
1457
|
/ix
|
|
1411
1458
|
|
|
1412
|
-
AUTH_SCOPE_RX = /
|
|
1413
|
-
\b(
|
|
1414
|
-
(?:in[-\s]?scope|authorized|authorised|engagement|written\s+permission|
|
|
1415
|
-
bug\s*bounty|explicit(?:ly)?\s+allowed|lab\s+only|my\s+lab|
|
|
1416
|
-
roe\b|rules?\s+of\s+engagement|i\s+own\s+this|owned\s+by\s+me|
|
|
1417
|
-
permission\s+to\s+(?:scan|test|probe)|scope:\s*\S+)
|
|
1418
|
-
)\b
|
|
1419
|
-
/ix
|
|
1420
|
-
|
|
1421
1459
|
public_class_method def self.request_intent(opts = {})
|
|
1422
1460
|
req = opts[:request].to_s
|
|
1423
1461
|
return :empty if req.strip.empty?
|
|
@@ -1467,19 +1505,6 @@ module PWN
|
|
|
1467
1505
|
:act
|
|
1468
1506
|
end
|
|
1469
1507
|
|
|
1470
|
-
public_class_method def self.recon_authorized?(opts = {})
|
|
1471
|
-
req = opts[:request].to_s
|
|
1472
|
-
return true if req.match?(AUTH_SCOPE_RX)
|
|
1473
|
-
|
|
1474
|
-
# Explicit engage flag from operator / REPL
|
|
1475
|
-
v = (PWN::Env.dig(:ai, :agent, :recon_authorized) if defined?(PWN::Env))
|
|
1476
|
-
return true if v == true || v.to_s =~ /\A(1|true|yes|on)\z/i
|
|
1477
|
-
|
|
1478
|
-
false
|
|
1479
|
-
rescue StandardError
|
|
1480
|
-
false
|
|
1481
|
-
end
|
|
1482
|
-
|
|
1483
1508
|
# Pure how-to: one text-only chat (no tools, no plan_first, no task recon).
|
|
1484
1509
|
# Deterministic greeting ack — no LLM, no tools, never mirror weather.
|
|
1485
1510
|
|
|
@@ -1591,6 +1616,8 @@ module PWN
|
|
|
1591
1616
|
txt = txt.to_s.strip
|
|
1592
1617
|
txt = 'I do not have enough context to answer that yet.' if txt.empty?
|
|
1593
1618
|
|
|
1619
|
+
debug_progress(msg: "final accepted chars=#{txt.length}")
|
|
1620
|
+
quiet_debug_tui!(reason: 'question')
|
|
1594
1621
|
append_session(session_id: session_id, role: 'user', content: request)
|
|
1595
1622
|
append_session(session_id: session_id, role: 'assistant', content: txt)
|
|
1596
1623
|
if defined?(Learning) && should_auto_introspect?(local: local_engine?, turn_fails: {}, iter: 0)
|
|
@@ -1605,6 +1632,7 @@ module PWN
|
|
|
1605
1632
|
end
|
|
1606
1633
|
txt
|
|
1607
1634
|
rescue StandardError => e
|
|
1635
|
+
quiet_debug_tui!(reason: 'question_error')
|
|
1608
1636
|
warn "[pwn-ai/loop] answer_question swallowed: #{e.class}: #{e.message}"
|
|
1609
1637
|
"Could not answer the question (#{e.class}: #{e.message})."
|
|
1610
1638
|
end
|
|
@@ -1652,8 +1680,6 @@ module PWN
|
|
|
1652
1680
|
Do NOT call tools. Do NOT plan multi-step work. Do NOT run scans,
|
|
1653
1681
|
probes, rubocop, rake, or host verification. Do NOT invent task
|
|
1654
1682
|
traces or internal planner monologue. Do NOT claim you ran anything.
|
|
1655
|
-
If the topic is network discovery, give safe lab examples and note
|
|
1656
|
-
that live sweeps need explicit in-scope authorization.
|
|
1657
1683
|
SYS
|
|
1658
1684
|
|
|
1659
1685
|
txt =
|
|
@@ -2007,6 +2033,9 @@ module PWN
|
|
|
2007
2033
|
request = opts[:request].to_s
|
|
2008
2034
|
session_id = opts[:session_id]
|
|
2009
2035
|
on_tool = opts[:on_tool]
|
|
2036
|
+
start_debug_session(opts)
|
|
2037
|
+
loud_debug_tui!(debug: opts[:debug])
|
|
2038
|
+
debug_progress(msg: "Loop.run start request=#{request[0, 240]}", debug: opts[:debug])
|
|
2010
2039
|
TurnFinalizer.enter_user_path! if defined?(TurnFinalizer)
|
|
2011
2040
|
engine = active_engine
|
|
2012
2041
|
local = local_engine?(engine: engine)
|
|
@@ -2026,23 +2055,23 @@ module PWN
|
|
|
2026
2055
|
OpenGoal.begin!(request: request, session_id: session_id)
|
|
2027
2056
|
end
|
|
2028
2057
|
Thread.current[:pwn_request_intent] = intent
|
|
2029
|
-
Thread.current[:pwn_recon_authorized] = recon_authorized?(request: request)
|
|
2030
2058
|
Thread.current[:pwn_extinguished] = {}
|
|
2059
|
+
debug_progress(msg: "intent=#{intent} engine=#{engine}", debug: opts[:debug])
|
|
2031
2060
|
expose_current_session(session_id: session_id)
|
|
2032
2061
|
Mistakes.check_user_correction(request: request, session_id: session_id) if defined?(Mistakes)
|
|
2033
2062
|
|
|
2034
2063
|
cheap = opts[:force_tools] != true && %i[greeting howto recall].include?(intent)
|
|
2035
2064
|
|
|
2036
|
-
# Greeting / light smalltalk: deterministic ack - no weather echo, no tools,
|
|
2037
|
-
# no PromptBuilder, no Registry.
|
|
2038
2065
|
if intent == :greeting && opts[:force_tools] != true
|
|
2066
|
+
debug_progress(msg: 'path=greeting', debug: opts[:debug])
|
|
2067
|
+
quiet_debug_tui!(debug: opts[:debug], reason: 'greeting')
|
|
2039
2068
|
return answer_greeting(
|
|
2040
2069
|
request: request,
|
|
2041
2070
|
session_id: session_id
|
|
2042
2071
|
)
|
|
2043
2072
|
end
|
|
2044
2073
|
|
|
2045
|
-
# Thin system prompt only for remaining cheap paths (howto/recall
|
|
2074
|
+
# Thin system prompt only for remaining cheap paths (howto/recall).
|
|
2046
2075
|
if cheap
|
|
2047
2076
|
system_role_content = opts[:system_role_content]
|
|
2048
2077
|
if system_role_content.nil? || system_role_content.to_s.empty?
|
|
@@ -2053,16 +2082,18 @@ module PWN
|
|
|
2053
2082
|
)
|
|
2054
2083
|
opts[:system_role_content] = system_role_content
|
|
2055
2084
|
end
|
|
2056
|
-
# How-to: never enter plan_first / task recon / tool thrash.
|
|
2057
2085
|
if intent == :howto
|
|
2086
|
+
debug_progress(msg: 'path=howto', debug: opts[:debug])
|
|
2087
|
+
quiet_debug_tui!(debug: opts[:debug], reason: 'howto')
|
|
2058
2088
|
return answer_howto(
|
|
2059
2089
|
request: request,
|
|
2060
2090
|
session_id: session_id,
|
|
2061
2091
|
system_role_content: system_role_content
|
|
2062
2092
|
)
|
|
2063
2093
|
end
|
|
2064
|
-
# Pure prior-turn / vague memory recall: one cheap path, no plan_first.
|
|
2065
2094
|
if intent == :recall
|
|
2095
|
+
debug_progress(msg: 'path=recall', debug: opts[:debug])
|
|
2096
|
+
quiet_debug_tui!(debug: opts[:debug], reason: 'recall')
|
|
2066
2097
|
return answer_recall(
|
|
2067
2098
|
request: request,
|
|
2068
2099
|
session_id: session_id,
|
|
@@ -2113,8 +2144,10 @@ module PWN
|
|
|
2113
2144
|
messages << { role: 'user', content: request }
|
|
2114
2145
|
append_session(session_id: session_id, role: 'user', content: request)
|
|
2115
2146
|
|
|
2116
|
-
|
|
2117
|
-
|
|
2147
|
+
trivia = world_knowledge?(request: request)
|
|
2148
|
+
# Every host-work request gets a task compass. Trivia does not —
|
|
2149
|
+
# inventing English tasks there keeps the model from emitting a final.
|
|
2150
|
+
task_summary_plan!(state: ts_state, request: request, on_tool: on_tool) if defined?(TaskSummarizer) && !trivia
|
|
2118
2151
|
# Re-bind tools from English plan so task list is the sole driver of
|
|
2119
2152
|
# tool exposure/ranking (Registry keyword router + CORE).
|
|
2120
2153
|
if ts_state.is_a?(Hash) && defined?(TaskSummarizer) && TaskSummarizer.respond_to?(:relevance_query)
|
|
@@ -2128,14 +2161,13 @@ module PWN
|
|
|
2128
2161
|
)
|
|
2129
2162
|
end
|
|
2130
2163
|
end
|
|
2131
|
-
# English-task-as-primary: inject tangible tasks only for
|
|
2132
|
-
inject_task_focus!(messages: messages, state: ts_state, force: true, request: request)
|
|
2164
|
+
# English-task-as-primary: inject tangible tasks only for host work.
|
|
2165
|
+
inject_task_focus!(messages: messages, state: ts_state, force: true, request: request) unless trivia
|
|
2133
2166
|
predicted = nil
|
|
2134
2167
|
Thread.current[:pwn_plan_predicted] = nil
|
|
2135
2168
|
cal_state = calibration_state
|
|
2136
2169
|
force_plan = cal_state[:force_plan]
|
|
2137
|
-
|
|
2138
|
-
skip_plan = %i[howto recall greeting].include?(intent)
|
|
2170
|
+
skip_plan = %i[howto recall greeting].include?(intent) || trivia
|
|
2139
2171
|
if !skip_plan && (force_plan || agent_flag(key: :plan_first, default: local) || budget_exhaustion_hot?) && !Array(tools).empty?
|
|
2140
2172
|
predicted = plan_first(messages: messages, request: request, ts_state: ts_state)
|
|
2141
2173
|
# P22 — prefer explicit return; fall back to thread stash
|
|
@@ -2154,7 +2186,7 @@ module PWN
|
|
|
2154
2186
|
)
|
|
2155
2187
|
end
|
|
2156
2188
|
end
|
|
2157
|
-
inject_task_focus!(messages: messages, state: ts_state, force: true, request: request)
|
|
2189
|
+
inject_task_focus!(messages: messages, state: ts_state, force: true, request: request) unless trivia
|
|
2158
2190
|
end
|
|
2159
2191
|
if budget_exhaustion_hot?
|
|
2160
2192
|
english_open = defined?(TaskSummarizer) && TaskSummarizer.respond_to?(:plan_open?) &&
|
|
@@ -2191,7 +2223,7 @@ module PWN
|
|
|
2191
2223
|
compact_history!(messages: messages) if local
|
|
2192
2224
|
# English-task-as-primary: when plan_idx advanced, tell the model
|
|
2193
2225
|
# which plain-English task is active before the next tool batch.
|
|
2194
|
-
inject_task_focus!(messages: messages, state: ts_state, request: request)
|
|
2226
|
+
inject_task_focus!(messages: messages, state: ts_state, request: request) unless trivia
|
|
2195
2227
|
|
|
2196
2228
|
# P17 — on the final iteration, strip tools and demand a plain-text
|
|
2197
2229
|
# answer. Without this the model happily emits one more tool_calls
|
|
@@ -2249,6 +2281,7 @@ module PWN
|
|
|
2249
2281
|
msg = call_engine(messages: messages, tools: last_iter ? nil : tools, ts_state: ts_state)
|
|
2250
2282
|
if msg.nil?
|
|
2251
2283
|
task_summary_flush!(state: ts_state, on_tool: on_tool)
|
|
2284
|
+
quiet_debug_tui!(reason: 'engine_empty')
|
|
2252
2285
|
return '[pwn-ai] engine returned no message'
|
|
2253
2286
|
end
|
|
2254
2287
|
|
|
@@ -2318,6 +2351,8 @@ module PWN
|
|
|
2318
2351
|
}
|
|
2319
2352
|
next
|
|
2320
2353
|
end
|
|
2354
|
+
debug_progress(msg: "final accepted chars=#{text.to_s.length}")
|
|
2355
|
+
quiet_debug_tui!(reason: 'final')
|
|
2321
2356
|
append_session(session_id: session_id, role: 'assistant', content: text)
|
|
2322
2357
|
Learning.auto_introspect(session_id: session_id, request: request, final: text, predicted: predicted, plan: ts_state && ts_state[:plan], ts_state: ts_state) if defined?(Learning) && should_auto_introspect?(local: local, turn_fails: turn_fails, iter: i)
|
|
2323
2358
|
maybe_finish_policy(session_id: session_id, proxy_ok: true, ts_state: ts_state)
|
|
@@ -2354,6 +2389,7 @@ module PWN
|
|
|
2354
2389
|
else
|
|
2355
2390
|
raw = Dispatch.call(tool_call: tc)
|
|
2356
2391
|
end
|
|
2392
|
+
debug_progress(msg: "dispatch #{name} ok_len=#{raw.to_s.length}")
|
|
2357
2393
|
tele = record_metrics(name: name, started: started, raw: raw, args: args, session_id: session_id, engine: engine, ts_state: ts_state)
|
|
2358
2394
|
result = Result.condition(content: raw, entry: entry)
|
|
2359
2395
|
|
|
@@ -2410,6 +2446,8 @@ module PWN
|
|
|
2410
2446
|
# failure (previously we only Mistakes.record'd and returned a bare
|
|
2411
2447
|
# string — no session row, no judge, no hindsight).
|
|
2412
2448
|
final_msg = '[pwn-ai] iteration budget exhausted'
|
|
2449
|
+
debug_progress(msg: 'final budget_exhausted')
|
|
2450
|
+
quiet_debug_tui!(reason: 'budget_exhausted')
|
|
2413
2451
|
if defined?(Mistakes)
|
|
2414
2452
|
Mistakes.record(
|
|
2415
2453
|
tool: 'agent_loop',
|
|
@@ -2464,8 +2502,8 @@ module PWN
|
|
|
2464
2502
|
how-to / usage questions → text-only explanation (no tools, no plan_first)
|
|
2465
2503
|
pure prior-turn recall ("what did I just say?") → answer_recall (no tools)
|
|
2466
2504
|
pure greeting / light smalltalk → answer_greeting (no tools, no weather echo)
|
|
2467
|
-
live
|
|
2468
|
-
|
|
2505
|
+
live sweeps are host-work goals like any other — pwn-ai does not
|
|
2506
|
+
decide authorization
|
|
2469
2507
|
Local-model scaffolding (PWN::Env[:ai][:agent]):
|
|
2470
2508
|
:plan_first - Boolean, plan-then-act pre-pass (default: local engine :ollama/:openwebui)
|
|
2471
2509
|
:tool_router - Boolean/nil, slim Registry.definitions (nil=auto on for ollama)
|
|
@@ -316,7 +316,7 @@ module PWN
|
|
|
316
316
|
# 2.5 — park unfixable sigs so nightly practice skips them
|
|
317
317
|
# Recoverable repeating failures get a structured fix instead of another
|
|
318
318
|
# fingerprint. Called from Loop after record so the loop extinguishes
|
|
319
|
-
# pain (placeholder / enoent
|
|
319
|
+
# pain (placeholder / enoent) rather than only logging it.
|
|
320
320
|
SHAPE_FIXES = {
|
|
321
321
|
'invalid_payload' => {
|
|
322
322
|
strategy: 'payload_schema',
|
|
@@ -343,10 +343,10 @@ module PWN
|
|
|
343
343
|
args_template: { command: 'command -v uname && uname -r' }
|
|
344
344
|
},
|
|
345
345
|
'exit126' => {
|
|
346
|
-
strategy: '
|
|
346
|
+
strategy: 'payload_schema',
|
|
347
347
|
tool: 'shell',
|
|
348
|
-
fix: '
|
|
349
|
-
args_template:
|
|
348
|
+
fix: 'exit 126 usually means the binary is not executable. Check command -v and file mode. Do not treat this as an authorization gate.',
|
|
349
|
+
args_template: { command: 'command -v uname && uname -r' }
|
|
350
350
|
},
|
|
351
351
|
'eacces' => {
|
|
352
352
|
strategy: 'payload_schema',
|
|
@@ -384,7 +384,7 @@ module PWN
|
|
|
384
384
|
hay = "#{m[:error]} #{m[:snippet]} #{m[:tool]}".downcase
|
|
385
385
|
recipe = nil if shape == 'handler_error' && !hay.match?(/command is required|argumenterror/)
|
|
386
386
|
recipe = nil if shape == 'exit127' && !hay.match?(/not found|\{\.\.\.\}|\{…\}/)
|
|
387
|
-
recipe = nil if shape == 'exit126' && !hay.match?(/
|
|
387
|
+
recipe = nil if shape == 'exit126' && !hay.match?(/cannot execute|not executable|is a directory/)
|
|
388
388
|
return m unless recipe && (force || count >= REPEAT_THRESHOLD) && !m[:resolved]
|
|
389
389
|
|
|
390
390
|
structured = {
|
|
@@ -434,7 +434,7 @@ module PWN
|
|
|
434
434
|
hay = "#{m[:error]} #{m[:snippet]} #{m[:shape]}".downcase
|
|
435
435
|
return 'handler_error' if hay.match?(/command is required|argumenterror/)
|
|
436
436
|
return 'exit127' if hay.match?(/not found|\{\.\.\.\}|\{…\}/)
|
|
437
|
-
return 'exit126' if hay.match?(/
|
|
437
|
+
return 'exit126' if hay.match?(/cannot execute|not executable|is a directory/)
|
|
438
438
|
return 'enoent' if hay.match?(/no such file|enoent/)
|
|
439
439
|
return 'eacces' if hay.match?(/permission denied|eacces|operation not permitted/)
|
|
440
440
|
return 'syntax' if hay.match?(/syntax error|unterminated/)
|
|
@@ -117,12 +117,11 @@ module PWN
|
|
|
117
117
|
RECENT TURNS block or one memory_recall — never a multi-tool plan.
|
|
118
118
|
Pure greetings / light smalltalk short-circuit to a fixed ack — never
|
|
119
119
|
echo weather or invent social filler ("noted, cloudy out there").
|
|
120
|
+
World-knowledge questions (what color is X) may be answered as a
|
|
121
|
+
text final with no tools. Tools stay available; do not invent a
|
|
122
|
+
host-work plan for them.
|
|
120
123
|
Do not treat process_sop_* or operator_pref_* memory about code
|
|
121
124
|
hygiene as the current user goal unless they asked to change code.
|
|
122
|
-
Live subnet sweeps, raw-socket discovery (hping3/nmap -sn mass
|
|
123
|
-
probes, etc.) require explicit in-scope / authorized engagement
|
|
124
|
-
language from the user. Without it, refuse the live scan and offer
|
|
125
|
-
the command syntax instead.
|
|
126
125
|
PROMPT
|
|
127
126
|
end
|
|
128
127
|
|
|
@@ -5,8 +5,7 @@ module PWN
|
|
|
5
5
|
module Agent
|
|
6
6
|
# Shared pre-dispatch guards for the two high-volume runtime tools
|
|
7
7
|
# (shell / pwn_eval). Rejects placeholder payloads, aliases wrong
|
|
8
|
-
# schema keys,
|
|
9
|
-
# shell that will actually run the command.
|
|
8
|
+
# schema keys, and names the shell that will actually run the command.
|
|
10
9
|
module ToolGuard
|
|
11
10
|
ALIASES = {
|
|
12
11
|
'command' => %w[value cmd input],
|
|
@@ -29,17 +28,6 @@ module PWN
|
|
|
29
28
|
|&>
|
|
30
29
|
/x
|
|
31
30
|
|
|
32
|
-
RECON_RX = %r{
|
|
33
|
-
(?:^|[;&|\s])(?:sudo\s+)?hping3?(?:\s|$).*(?:-1|--icmp|--flood|/[0-9]{1,2}|seq\s+|for\s+)
|
|
34
|
-
|(?:^|[;&|\s])(?:sudo\s+)?nmap\b[^\n]*(?:-sn|-sP|-PE|-PP|-PM)[^\n]*(?:/[0-9]{1,2}|\d+\.\d+\.\d+\.\d+)
|
|
35
|
-
|(?:^|[;&|\s])(?:sudo\s+)?masscan\b
|
|
36
|
-
|(?:^|[;&|\s])(?:sudo\s+)?nping\b
|
|
37
|
-
|(?:^|[;&|\s])(?:sudo\s+)?fping\b[^\n]*/
|
|
38
|
-
|for\s+\w+\s+in\s+\$?\(?seq[^)]*\)?[^;]*;\s*do\s[^;]*(?:hping|ping\s+-c|nmap)
|
|
39
|
-
|PWN::Plugins::NmapIt
|
|
40
|
-
|\bNmapIt\.(?:scan|new|help)
|
|
41
|
-
}ix
|
|
42
|
-
|
|
43
31
|
public_class_method def self.present?(opts = {})
|
|
44
32
|
value = opts.is_a?(Hash) ? opts[:value] : opts
|
|
45
33
|
!value.nil? && !value.to_s.strip.empty?
|
|
@@ -57,23 +45,6 @@ module PWN
|
|
|
57
45
|
false
|
|
58
46
|
end
|
|
59
47
|
|
|
60
|
-
public_class_method def self.recon_text?(opts = {})
|
|
61
|
-
RECON_RX.match?(opts[:text].to_s)
|
|
62
|
-
rescue StandardError
|
|
63
|
-
false
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
public_class_method def self.recon_authorized?(opts = {})
|
|
67
|
-
return true if Thread.current[:pwn_recon_authorized] == true
|
|
68
|
-
return true if defined?(Loop) && Loop.respond_to?(:recon_authorized?) &&
|
|
69
|
-
Loop.recon_authorized?(request: opts[:request].to_s)
|
|
70
|
-
|
|
71
|
-
v = (PWN::Env.dig(:ai, :agent, :recon_authorized) if defined?(PWN::Env))
|
|
72
|
-
v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
|
|
73
|
-
rescue StandardError
|
|
74
|
-
false
|
|
75
|
-
end
|
|
76
|
-
|
|
77
48
|
public_class_method def self.shell_bash?
|
|
78
49
|
v = (PWN::Env.dig(:ai, :agent, :shell_bash) if defined?(PWN::Env))
|
|
79
50
|
v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
|
|
@@ -123,21 +94,6 @@ module PWN
|
|
|
123
94
|
}
|
|
124
95
|
end
|
|
125
96
|
|
|
126
|
-
public_class_method def self.recon_blocked(opts = {})
|
|
127
|
-
refused = opts[:text].to_s[0, 200]
|
|
128
|
-
msg = '[pwn-ai/guard] blocked unauthorized recon/sweep. ' \
|
|
129
|
-
'Need explicit in-scope authorization on the user request or ' \
|
|
130
|
-
'PWN::Env[:ai][:agent][:recon_authorized]=true. ' \
|
|
131
|
-
"Refused: #{refused}"
|
|
132
|
-
{
|
|
133
|
-
stdout: '',
|
|
134
|
-
stderr: msg,
|
|
135
|
-
exit: 126,
|
|
136
|
-
error: 'unauthorized_recon_blocked',
|
|
137
|
-
hint: msg
|
|
138
|
-
}
|
|
139
|
-
end
|
|
140
|
-
|
|
141
97
|
public_class_method def self.authors
|
|
142
98
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
143
99
|
end
|
|
@@ -146,7 +102,6 @@ module PWN
|
|
|
146
102
|
puts <<~USAGE
|
|
147
103
|
USAGE:
|
|
148
104
|
PWN::AI::Agent::ToolGuard.placeholder?(text: '...')
|
|
149
|
-
PWN::AI::Agent::ToolGuard.recon_authorized?
|
|
150
105
|
PWN::AI::Agent::ToolGuard.coerce_args(args: { value: 'id' }, required: %w[command])
|
|
151
106
|
#{self}.authors
|
|
152
107
|
USAGE
|
|
@@ -46,16 +46,6 @@ PWN::AI::Agent::Registry.register(
|
|
|
46
46
|
}
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
if PWN::AI::Agent::ToolGuard.recon_text?(text: code) &&
|
|
50
|
-
!PWN::AI::Agent::ToolGuard.recon_authorized?
|
|
51
|
-
blocked = PWN::AI::Agent::ToolGuard.recon_blocked(text: code)
|
|
52
|
-
return {
|
|
53
|
-
stdout: '',
|
|
54
|
-
error: blocked[:error],
|
|
55
|
-
hint: blocked[:hint]
|
|
56
|
-
}
|
|
57
|
-
end
|
|
58
|
-
|
|
59
49
|
old_stdout = $stdout
|
|
60
50
|
buf = StringIO.new
|
|
61
51
|
$stdout = buf
|
|
@@ -63,12 +63,6 @@ PWN::AI::Agent::Registry.register(
|
|
|
63
63
|
)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
# Guard: raw-socket / subnet discovery without explicit engagement auth.
|
|
67
|
-
if PWN::AI::Agent::ToolGuard.recon_text?(text: cmd) &&
|
|
68
|
-
!PWN::AI::Agent::ToolGuard.recon_authorized?
|
|
69
|
-
return PWN::AI::Agent::ToolGuard.recon_blocked(text: cmd)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
66
|
stdout = +''
|
|
73
67
|
stderr = +''
|
|
74
68
|
exitstatus = nil
|