lictor-mcp 0.1.0
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 +7 -0
- data/exe/lictor-mcp +12 -0
- data/home/runner/work/lictor/lictor/mcp_server/exe/lictor-mcp +12 -0
- data/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp/server.rb +230 -0
- data/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp/version.rb +5 -0
- data/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp.rb +9 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 29c5c04710c3fecad7fd15dcd98cf622587246be8cb12fb00a72b8b6a0b78161
|
|
4
|
+
data.tar.gz: 9ca331224ec30c255338d4f46927bbaa6b85387af5cee178e3bef919d848541a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4d5642b38805cd2d4932034e7b5424c7c05e87d977816fa17bfb256d7230af2cd7fdb8ff32fe7f71df53ada87a6d3285427a448b1e2c4ffc7598f5fd82ef04ba
|
|
7
|
+
data.tar.gz: 14cb858b0170bc2fbc714affb40568f3d1b35cef706041503eeb3b0c37958a954ca2e037e289d0f253a661bf1613e04096663e0f619b8bfe371c37a8a5de4166
|
data/exe/lictor-mcp
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
unless defined?(Bundler)
|
|
5
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __dir__)
|
|
6
|
+
require "bundler/setup"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require "lictor-mcp"
|
|
10
|
+
|
|
11
|
+
transport = MCP::Server::Transports::StdioTransport.new(LictorMcp::Server.build)
|
|
12
|
+
transport.open
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
unless defined?(Bundler)
|
|
5
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __dir__)
|
|
6
|
+
require "bundler/setup"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require "lictor-mcp"
|
|
10
|
+
|
|
11
|
+
transport = MCP::Server::Transports::StdioTransport.new(LictorMcp::Server.build)
|
|
12
|
+
transport.open
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
require "lictor"
|
|
5
|
+
|
|
6
|
+
module LictorMcp
|
|
7
|
+
# The MCP surface of the orchestrator: the LLM harness's hands.
|
|
8
|
+
# Every tool runs server-side - the pack armor, the corpus guard,
|
|
9
|
+
# the trail (0600). The agent proposes; augure disposes; lictor
|
|
10
|
+
# executes; the trail records everything.
|
|
11
|
+
module ToolHelpers
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def respond(payload)
|
|
15
|
+
::MCP::Tool::Response.new([{type: "text", text: ::JSON.generate(payload)}])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def error(message)
|
|
19
|
+
::MCP::Tool::Response.new([{type: "text", text: ::JSON.generate(error: message)}])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def trail
|
|
23
|
+
::Lictor::Trail.new(::Lictor::Config.load[:trail] || "lictor-mcp-trail.jsonl")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Server
|
|
28
|
+
include ToolHelpers
|
|
29
|
+
|
|
30
|
+
TOOLS = [
|
|
31
|
+
ScanTarget = Class.new(::MCP::Tool) do
|
|
32
|
+
description "Run nmap against a target and return the target inventory."
|
|
33
|
+
input_schema(properties: {
|
|
34
|
+
host: {type: "string"}
|
|
35
|
+
}, required: %w[host])
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
include ::LictorMcp::ToolHelpers
|
|
39
|
+
|
|
40
|
+
def call(host:, server_context: nil)
|
|
41
|
+
targets = nil
|
|
42
|
+
Dir.mktmpdir do |d|
|
|
43
|
+
targets = ::Lictor::Scanner.run(host, out: File.join(d, "s.xml"))
|
|
44
|
+
end
|
|
45
|
+
trail.record(run: SecureRandom.uuid, type: "scan", target: host)
|
|
46
|
+
respond(targets: targets, candidates: targets.map { |t| "#{t[:host]}:#{t[:port]}" })
|
|
47
|
+
rescue ::Lictor::ScanError => e
|
|
48
|
+
error(e.message)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end,
|
|
52
|
+
|
|
53
|
+
PlanTechnique = Class.new(::MCP::Tool) do
|
|
54
|
+
description "Decide on facts (or a binary - profiled automatically) and return the " \
|
|
55
|
+
"auditable decision with rule provenance. Augure disposes: the agent " \
|
|
56
|
+
"augure decides - you cannot pick an unverified technique."
|
|
57
|
+
input_schema(properties: {
|
|
58
|
+
target: {type: "string", description: "binary path or .facts path"}
|
|
59
|
+
}, required: %w[target])
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
include ::LictorMcp::ToolHelpers
|
|
63
|
+
|
|
64
|
+
def call(target:, server_context: nil)
|
|
65
|
+
facts = ::Lictor::Profiler.elf?(target) ? ::Lictor::Profiler.run(target) : File.read(target)
|
|
66
|
+
packs = ::Lictor::TechniquePacks.load(::Lictor::Config.load[:packs] || "packs")
|
|
67
|
+
decision = ::Lictor::Handoff.plan(facts: facts, priors: ::Lictor::Priors.from_trail(trail), packs: packs)
|
|
68
|
+
trail.record(run: SecureRandom.uuid, type: "plan", selected: decision[:selected],
|
|
69
|
+
applicable: decision[:applicable])
|
|
70
|
+
respond(decision)
|
|
71
|
+
rescue ::Augure::MalformedFact, ::Augure::UnknownPredicate, ::Lictor::ProfilerError => e
|
|
72
|
+
error(e.message)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end,
|
|
76
|
+
|
|
77
|
+
ExecuteTechnique = Class.new(::MCP::Tool) do
|
|
78
|
+
description "Execute the decided technique against the target. " \
|
|
79
|
+
"The executor discovers exploits from the operator's repos; it never " \
|
|
80
|
+
"invents one. The trail records what actually executed."
|
|
81
|
+
input_schema(properties: {
|
|
82
|
+
target: {type: "string"}, technique: {type: "string"},
|
|
83
|
+
exploit_name: {type: "string", description: "from your repos (ronin-exploits list)"},
|
|
84
|
+
payload_file: {type: "string", description: "a pre-built payload from lictor build"}
|
|
85
|
+
}, required: %w[target technique exploit_name])
|
|
86
|
+
|
|
87
|
+
class << self
|
|
88
|
+
include ::LictorMcp::ToolHelpers
|
|
89
|
+
|
|
90
|
+
def call(target:, technique:, exploit_name:, payload_file: nil, server_context: nil)
|
|
91
|
+
executor = ::Lictor::Executor.resolve(:ronin)
|
|
92
|
+
result = executor.execute(target: target, technique: technique, recipe: {console: ""},
|
|
93
|
+
exploit_name: exploit_name, payload: payload_file)
|
|
94
|
+
trail.record(run: SecureRandom.uuid, type: "outcome", technique: technique,
|
|
95
|
+
success: result[:success] == true, executed: result[:executed],
|
|
96
|
+
target: target)
|
|
97
|
+
respond(result)
|
|
98
|
+
rescue ::Lictor::ExecutorError => e
|
|
99
|
+
error(e.message)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end,
|
|
103
|
+
|
|
104
|
+
ValidatePack = Class.new(::MCP::Tool) do
|
|
105
|
+
description "Validate a technique pack through the full armor (schema, stratification, " \
|
|
106
|
+
"corpus guard). Installing needs the operator's " \
|
|
107
|
+
"interactive confirmation - validation alone is safe."
|
|
108
|
+
input_schema(properties: {pack_path: {type: "string"}}, required: %w[pack_path])
|
|
109
|
+
|
|
110
|
+
class << self
|
|
111
|
+
include ::LictorMcp::ToolHelpers
|
|
112
|
+
|
|
113
|
+
def call(pack_path:, server_context: nil)
|
|
114
|
+
pack = ::Augure::PackLoader.load_file(pack_path)
|
|
115
|
+
::Augure::PackLoader.corpus_guard([pack])
|
|
116
|
+
respond(valid: true, technique: pack.technique, author: pack.author, source: pack.source)
|
|
117
|
+
rescue ::Augure::PackError => e
|
|
118
|
+
error("pack refused: #{e.message}")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end,
|
|
122
|
+
BuildPayload = Class.new(::MCP::Tool) do
|
|
123
|
+
description "Assemble the payload for the decided technique: the offset is " \
|
|
124
|
+
"MEASURED (static frame read, the debugger as fallback), the return " \
|
|
125
|
+
"address comes from the facts, the shellcode is assembly source. " \
|
|
126
|
+
"Writes a payload file and trails the build."
|
|
127
|
+
input_schema(properties: {
|
|
128
|
+
binary: {type: "string", description: "the acquired local ELF copy"},
|
|
129
|
+
facts_path: {type: "string", description: "the .facts file"},
|
|
130
|
+
technique: {type: "string", description: "defaults to the plan's selection"},
|
|
131
|
+
payload_file: {type: "string", description: "custom shellcode/payload bytes"}
|
|
132
|
+
}, required: %w[binary facts_path])
|
|
133
|
+
|
|
134
|
+
class << self
|
|
135
|
+
include ::LictorMcp::ToolHelpers
|
|
136
|
+
|
|
137
|
+
def call(binary:, facts_path:, technique: nil, payload_file: nil, server_context: nil)
|
|
138
|
+
facts = ::File.read(facts_path)
|
|
139
|
+
packs = ::Lictor::TechniquePacks.load(::Lictor::Config.load[:packs] || "packs")
|
|
140
|
+
technique ||= ::Lictor::Handoff.plan(facts: facts, packs: packs)[:selected]
|
|
141
|
+
raise ::Lictor::PayloadBuilder::BuildError, "no technique selected - plan decided nothing" unless technique
|
|
142
|
+
|
|
143
|
+
result = ::Lictor::PayloadBuilder.build(binary: binary, facts: facts,
|
|
144
|
+
technique: technique, out: ::File.join(::Dir.tmpdir, "lictor-payload-#{::Process.pid}.bin"),
|
|
145
|
+
payload_file: payload_file)
|
|
146
|
+
trail.record(run: ::SecureRandom.uuid, type: "build", technique: technique,
|
|
147
|
+
offset: result[:offset], offset_source: result[:offset_source],
|
|
148
|
+
ret: result[:ret_source], size: result[:size], out: result[:path])
|
|
149
|
+
respond(result)
|
|
150
|
+
rescue ::Lictor::PayloadBuilder::BuildError, ::Augure::MalformedFact, ::Augure::UnknownPredicate => e
|
|
151
|
+
error(e.message)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end,
|
|
155
|
+
|
|
156
|
+
ScaffoldExploit = Class.new(::MCP::Tool) do
|
|
157
|
+
description "Draft a ronin exploit for the decided technique, carrying the measured " \
|
|
158
|
+
"constants: the file lands in the staging directory and the OPERATOR " \
|
|
159
|
+
"reviews and commits it. The exploit NAME follows the discovery convention."
|
|
160
|
+
input_schema(properties: {
|
|
161
|
+
binary: {type: "string"},
|
|
162
|
+
facts_path: {type: "string"},
|
|
163
|
+
technique: {type: "string"},
|
|
164
|
+
staging_dir: {type: "string", description: "where the draft lands (operator-reviewed)"}
|
|
165
|
+
}, required: %w[binary facts_path technique staging_dir])
|
|
166
|
+
|
|
167
|
+
class << self
|
|
168
|
+
include ::LictorMcp::ToolHelpers
|
|
169
|
+
|
|
170
|
+
def call(binary:, facts_path:, technique:, staging_dir:, server_context: nil)
|
|
171
|
+
# The scaffold touches no target: the staging directory is
|
|
172
|
+
# where the draft lands, the operator reviews, the trail
|
|
173
|
+
# records.
|
|
174
|
+
facts = ::File.read(facts_path)
|
|
175
|
+
result = ::Lictor::PayloadBuilder.build(binary: binary, facts: facts,
|
|
176
|
+
technique: technique, out: ::File.join(::Dir.tmpdir, "lictor-payload-#{::Process.pid}.bin"))
|
|
177
|
+
dest = ::Lictor::ExploitScaffold.scaffold(technique: technique, constants: result,
|
|
178
|
+
repo_dir: staging_dir, author: "lictor-mcp scaffold (operator review required)")
|
|
179
|
+
trail.record(run: ::SecureRandom.uuid, type: "scaffold", technique: technique,
|
|
180
|
+
offset: result[:offset], ret: result[:ret_source], file: dest)
|
|
181
|
+
respond(scaffolded: dest, review: "the operator reads and commits this file")
|
|
182
|
+
rescue ::Lictor::PayloadBuilder::BuildError, ::Lictor::ExploitScaffold::ScaffoldError,
|
|
183
|
+
::Augure::MalformedFact, ::Augure::UnknownPredicate => e
|
|
184
|
+
error(e.message)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end,
|
|
188
|
+
|
|
189
|
+
SuggestFacts = Class.new(::MCP::Tool) do
|
|
190
|
+
description "Submit hypothesis facts on top of an existing fact base; the closed " \
|
|
191
|
+
"schema is the whitelist (no applicable() exists to forge), augure " \
|
|
192
|
+
"re-decides on the merged base, and the delta comes back. The trail " \
|
|
193
|
+
"records who proposed what and what changed."
|
|
194
|
+
input_schema(properties: {
|
|
195
|
+
facts_path: {type: "string", description: "path to the base .facts file"},
|
|
196
|
+
suggested_facts: {type: "string", description: "hypothesis facts, one predicate(\"atom\"). per line"}
|
|
197
|
+
}, required: %w[facts_path suggested_facts])
|
|
198
|
+
|
|
199
|
+
class << self
|
|
200
|
+
include ::LictorMcp::ToolHelpers
|
|
201
|
+
|
|
202
|
+
def call(facts_path:, suggested_facts:, server_context: nil)
|
|
203
|
+
result = ::Lictor::Suggestion.apply(base_facts: File.read(facts_path),
|
|
204
|
+
suggested: suggested_facts, trail: trail, target: facts_path)
|
|
205
|
+
respond(target: facts_path, delta: {
|
|
206
|
+
added: result[:added],
|
|
207
|
+
applicable_before: result[:applicable_before],
|
|
208
|
+
applicable_after: result[:applicable_after],
|
|
209
|
+
selected_before: result[:selected_before],
|
|
210
|
+
selected_after: result[:selected_after]
|
|
211
|
+
})
|
|
212
|
+
rescue ::Augure::MalformedFact => e
|
|
213
|
+
# the parse error embeds the offending line: a file named by
|
|
214
|
+
# the caller must not leak its content through the error
|
|
215
|
+
error(e.message.sub(/line \d+: malformed fact .*/, "line ?"))
|
|
216
|
+
rescue ::Augure::UnknownPredicate => e
|
|
217
|
+
error(e.message)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
].freeze
|
|
222
|
+
|
|
223
|
+
def self.build
|
|
224
|
+
::MCP::Server.new(name: "lictor-mcp", version: ::LictorMcp::VERSION, tools: TOOLS).tap do |server|
|
|
225
|
+
server.instructions = "lictor orchestrates the augure decision layer. The operator is " \
|
|
226
|
+
"the authorization; the trail is the record."
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lictor-mcp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Gregory Epp
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: lictor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.1.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: mcp
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.6'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.6'
|
|
40
|
+
description: 'MCP tools over the lictor orchestrator: scan, plan, execute, validate_pack,
|
|
41
|
+
build_payload, scaffold_exploit, suggest_facts - validated SERVER-side (the pack
|
|
42
|
+
armor, the corpus guard, the trail). The LLM proposes; augure disposes; lictor executes;
|
|
43
|
+
the trail records.'
|
|
44
|
+
email:
|
|
45
|
+
- log0u7@users.noreply.github.com
|
|
46
|
+
executables:
|
|
47
|
+
- lictor-mcp
|
|
48
|
+
extensions: []
|
|
49
|
+
extra_rdoc_files: []
|
|
50
|
+
files:
|
|
51
|
+
- "/home/runner/work/lictor/lictor/mcp_server/exe/lictor-mcp"
|
|
52
|
+
- "/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp.rb"
|
|
53
|
+
- "/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp/server.rb"
|
|
54
|
+
- "/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp/version.rb"
|
|
55
|
+
- exe/lictor-mcp
|
|
56
|
+
homepage: https://github.com/log0u7/lictor
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://github.com/log0u7/lictor
|
|
61
|
+
source_code_uri: https://github.com/log0u7/lictor
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '3.4'
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 3.6.9
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: MCP surface for the lictor orchestrator - the LLM harness's hands, server-side
|
|
79
|
+
validated.
|
|
80
|
+
test_files: []
|