@eyeauras/insight-cli 0.3.1 → 0.4.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.
Files changed (152) hide show
  1. package/.cli-package-build.json +1 -1
  2. package/SKILL.md +4 -1
  3. package/dist/src/analysis-default.d.ts +26 -0
  4. package/dist/src/analysis-default.js +149 -0
  5. package/dist/src/analysis-storage.d.ts +39 -20
  6. package/dist/src/analysis-storage.js +112 -16
  7. package/dist/src/arguments.js +9 -4
  8. package/dist/src/cli.js +45 -37
  9. package/dist/src/code-references.d.ts +48 -0
  10. package/dist/src/code-references.js +154 -0
  11. package/dist/src/context-diagnostics.d.ts +25 -0
  12. package/dist/src/context-diagnostics.js +82 -0
  13. package/dist/src/contexts.d.ts +13 -1
  14. package/dist/src/contexts.js +38 -9
  15. package/dist/src/function-boundaries.d.ts +9 -0
  16. package/dist/src/function-boundaries.js +68 -0
  17. package/dist/src/function-entries.d.ts +33 -0
  18. package/dist/src/function-entries.js +84 -0
  19. package/dist/src/function-inspection.d.ts +4 -2
  20. package/dist/src/function-inspection.js +94 -59
  21. package/dist/src/function-results.d.ts +3 -1
  22. package/dist/src/function-results.js +9 -3
  23. package/dist/src/inline-script.d.ts +15 -0
  24. package/dist/src/inline-script.js +62 -0
  25. package/dist/src/internal-timing.d.ts +3 -0
  26. package/dist/src/internal-timing.js +52 -0
  27. package/dist/src/lifecycle.js +14 -4
  28. package/dist/src/native-inputs.d.ts +1 -1
  29. package/dist/src/native-inputs.js +1 -1
  30. package/dist/src/native-queries.js +1 -1
  31. package/dist/src/persistent-client.d.ts +30 -0
  32. package/dist/src/persistent-client.js +125 -0
  33. package/dist/src/queries.js +4 -1
  34. package/dist/src/query-policy.js +9 -1
  35. package/dist/src/research-view.d.ts +6 -0
  36. package/dist/src/research-view.js +9 -0
  37. package/dist/src/research.d.ts +38 -0
  38. package/dist/src/research.js +78 -8
  39. package/dist/src/string-anchors.d.ts +43 -0
  40. package/dist/src/string-anchors.js +176 -0
  41. package/dist/src/sync-policy.js +3 -0
  42. package/dist/src/timed-backend.d.ts +3 -0
  43. package/dist/src/timed-backend.js +16 -0
  44. package/dist/src/transport.d.ts +1 -0
  45. package/dist/src/transport.js +4 -0
  46. package/dist/src/workspace.js +5 -2
  47. package/package.json +12 -1
  48. package/references/native/InsightAnchorScan.java +135 -0
  49. package/references/native/InsightCodeReferences.java +129 -0
  50. package/references/native/InsightFunctionBoundaries.java +56 -0
  51. package/references/native/InsightInlineExecutor.java +143 -0
  52. package/references/native/InsightInlineScript.java +14 -0
  53. package/references/native/InsightPeRanges.java +92 -0
  54. package/references/native/InsightResearchQueries.java +22 -1
  55. package/references/native/InsightResearchView.java +69 -0
  56. package/references/native/InsightStringAnchors.java +49 -0
  57. package/references/persistent-client.mjs +125 -0
  58. package/references/persistent.md +50 -0
  59. package/references/query-semantics.md +12 -0
  60. package/references/recovery.md +36 -0
  61. package/references/research.md +87 -0
  62. package/references/setup.md +1 -1
  63. package/references/sparse-navigation.md +173 -0
  64. package/references/storage.md +35 -7
  65. package/runtime/GhidraMCP-7.0.0.jar +0 -0
  66. package/runtime/GhidraMCP-NOTICE +5 -0
  67. package/runtime/provenance.json +17 -3
  68. package/runtime/research-view.provenance.json +28 -0
  69. package/runtime/toolbox.json +10 -3
  70. package/skill-bundle/insight-artifacts/SKILL.md +1 -1
  71. package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
  72. package/skill-bundle/insight-artifacts/references/storage.md +35 -7
  73. package/skill-bundle/insight-binary/SKILL.md +1 -1
  74. package/skill-bundle/insight-capture/SKILL.md +1 -1
  75. package/skill-bundle/insight-cli/SKILL.md +4 -1
  76. package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
  77. package/skill-bundle/insight-cli/references/persistent.md +50 -0
  78. package/skill-bundle/insight-cli/references/recovery.md +36 -0
  79. package/skill-bundle/insight-cli/references/setup.md +1 -1
  80. package/skill-bundle/insight-cli/references/storage.md +35 -7
  81. package/skill-bundle/insight-die/SKILL.md +1 -1
  82. package/skill-bundle/insight-dnspy/SKILL.md +1 -1
  83. package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
  84. package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
  85. package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
  86. package/skill-bundle/insight-frida/SKILL.md +1 -1
  87. package/skill-bundle/insight-ghidra/SKILL.md +14 -1
  88. package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
  89. package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
  90. package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
  91. package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
  92. package/skill-bundle/insight-ghidra/references/research.md +87 -0
  93. package/skill-bundle/insight-ghidra/references/setup.md +1 -1
  94. package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
  95. package/skill-bundle/insight-ghidra/references/storage.md +35 -7
  96. package/skill-bundle/insight-ilspy/SKILL.md +1 -1
  97. package/skill-bundle/insight-interop/SKILL.md +1 -1
  98. package/skill-bundle/insight-lldb/SKILL.md +1 -1
  99. package/skill-bundle/insight-llvm/SKILL.md +1 -1
  100. package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
  101. package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
  102. package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
  103. package/skill-bundle/insight-overview/SKILL.md +1 -1
  104. package/skill-bundle/insight-procmon/SKILL.md +1 -1
  105. package/skill-bundle/insight-re/SKILL.md +4 -1
  106. package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
  107. package/skill-bundle/insight-re/references/persistent.md +50 -0
  108. package/skill-bundle/insight-re/references/recovery.md +36 -0
  109. package/skill-bundle/insight-re/references/storage.md +35 -7
  110. package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
  111. package/skill-bundle/insight-strace/SKILL.md +1 -1
  112. package/skill-bundle/insight-tshark/SKILL.md +1 -1
  113. package/skill-bundle/insight-windbg/SKILL.md +1 -1
  114. package/skill-bundle/manifest.json +1 -1
  115. package/skills/insight-artifacts/SKILL.md +1 -1
  116. package/skills/insight-artifacts/references/recovery.md +36 -0
  117. package/skills/insight-artifacts/references/storage.md +35 -7
  118. package/skills/insight-binary/SKILL.md +1 -1
  119. package/skills/insight-capture/SKILL.md +1 -1
  120. package/skills/insight-die/SKILL.md +1 -1
  121. package/skills/insight-dnspy/SKILL.md +1 -1
  122. package/skills/insight-dotnet-dump/SKILL.md +1 -1
  123. package/skills/insight-dotnet-stack/SKILL.md +1 -1
  124. package/skills/insight-dotnet-trace/SKILL.md +1 -1
  125. package/skills/insight-frida/SKILL.md +1 -1
  126. package/skills/insight-ghidra/SKILL.md +14 -1
  127. package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
  128. package/skills/insight-ghidra/references/persistent.md +50 -0
  129. package/skills/insight-ghidra/references/query-semantics.md +12 -0
  130. package/skills/insight-ghidra/references/recovery.md +36 -0
  131. package/skills/insight-ghidra/references/research.md +87 -0
  132. package/skills/insight-ghidra/references/setup.md +1 -1
  133. package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
  134. package/skills/insight-ghidra/references/storage.md +35 -7
  135. package/skills/insight-ilspy/SKILL.md +1 -1
  136. package/skills/insight-interop/SKILL.md +1 -1
  137. package/skills/insight-lldb/SKILL.md +1 -1
  138. package/skills/insight-llvm/SKILL.md +1 -1
  139. package/skills/insight-managed-heap/SKILL.md +1 -1
  140. package/skills/insight-managed-metadata/SKILL.md +1 -1
  141. package/skills/insight-mitmproxy/SKILL.md +1 -1
  142. package/skills/insight-overview/SKILL.md +1 -1
  143. package/skills/insight-procmon/SKILL.md +1 -1
  144. package/skills/insight-re/SKILL.md +4 -1
  145. package/skills/insight-re/references/persistent-client.mjs +125 -0
  146. package/skills/insight-re/references/persistent.md +50 -0
  147. package/skills/insight-re/references/recovery.md +36 -0
  148. package/skills/insight-re/references/storage.md +35 -7
  149. package/skills/insight-sevenzip/SKILL.md +1 -1
  150. package/skills/insight-strace/SKILL.md +1 -1
  151. package/skills/insight-tshark/SKILL.md +1 -1
  152. package/skills/insight-windbg/SKILL.md +1 -1
@@ -0,0 +1,143 @@
1
+ package insight;
2
+
3
+ import com.google.gson.*;
4
+ import generic.jar.ResourceFile;
5
+ import ghidra.app.script.*;
6
+ import ghidra.util.task.TaskMonitor;
7
+ import java.io.*;
8
+ import java.lang.reflect.*;
9
+ import java.net.*;
10
+ import java.nio.charset.StandardCharsets;
11
+ import java.nio.channels.FileChannel;
12
+ import java.nio.file.*;
13
+ import java.security.MessageDigest;
14
+ import java.util.*;
15
+ import java.util.regex.*;
16
+ import javax.tools.*;
17
+
18
+ /** Owned Java-only inline executor. Never infers non-execution from exception text or stdout. */
19
+ public final class InsightInlineExecutor {
20
+ static final int OUTPUT_BYTES=32768, MAX_DIAGNOSTICS=100;
21
+ static final Gson JSON=new GsonBuilder().serializeNulls().create();
22
+ static final class BoundedOutput extends OutputStream {
23
+ final ByteArrayOutputStream bytes=new ByteArrayOutputStream(); boolean truncated;
24
+ public synchronized void write(int b){if(bytes.size()<OUTPUT_BYTES)bytes.write(b);else truncated=true;}
25
+ public synchronized void write(byte[] b,int start,int length){int n=Math.min(length,OUTPUT_BYTES-bytes.size());bytes.write(b,start,n);if(n<length)truncated=true;}
26
+ synchronized String text(){return bytes.toString(StandardCharsets.UTF_8);}
27
+ synchronized Map<String,Object> value(){return Map.of("text",text(),"truncated",truncated,"maximumBytes",OUTPUT_BYTES);}
28
+ }
29
+ record Source(String name,String text,int prefixLines,int userLines,boolean wrapped) {}
30
+ /** Keep offsets/newlines, but hide comments and literals from declaration/import recognition. */
31
+ static String lexical(String s){
32
+ char[] out=s.toCharArray();int state=0;
33
+ for(int i=0;i<s.length();i++){
34
+ char c=s.charAt(i),n=i+1<s.length()?s.charAt(i+1):0;
35
+ if(state==0){
36
+ if(c=='/'&&n=='/'){out[i]=out[++i]=' ';state=1;}
37
+ else if(c=='/'&&n=='*'){out[i]=out[++i]=' ';state=2;}
38
+ else if(c=='"'&&i+2<s.length()&&s.startsWith("\"\"\"",i)){out[i]=out[i+1]=out[i+2]=' ';i+=2;state=5;}
39
+ else if(c=='"'||c=='\''){out[i]=' ';state=c=='"'?3:4;}
40
+ }else{
41
+ if(c!='\n'&&c!='\r')out[i]=' ';
42
+ if(state==1&&c=='\n')state=0;
43
+ else if(state==2&&c=='*'&&n=='/'){out[++i]=' ';state=0;}
44
+ else if((state==3||state==4)&&c=='\\'&&i+1<s.length()){if(s.charAt(i+1)!='\n')out[++i]=' ';}
45
+ else if((state==3&&c=='"')||(state==4&&c=='\''))state=0;
46
+ else if(state==5&&s.startsWith("\"\"\"",i)){out[i]=out[i+1]=out[i+2]=' ';i+=2;state=0;}
47
+ }
48
+ }
49
+ return new String(out);
50
+ }
51
+ static Source source(String code,String attempt){
52
+ String lex=lexical(code);
53
+ Matcher type=Pattern.compile("\\bpublic\\s+(?:(?:abstract|final|strictfp|sealed|non-sealed)\\s+)*(?:class|interface|enum|record)\\s+([A-Za-z_$][\\w$]*)").matcher(lex);
54
+ if(type.find()){
55
+ Matcher pkg=Pattern.compile("\\bpackage\\s+([A-Za-z_$][\\w$]*(?:\\s*\\.\\s*[A-Za-z_$][\\w$]*)*)\\s*;").matcher(lex);
56
+ String prefix=pkg.find()?pkg.group(1).replaceAll("\\s","")+".":"";
57
+ return new Source(prefix+type.group(1),code,0,code.split("\n",-1).length,false);
58
+ }
59
+ StringBuilder imports=new StringBuilder("import ghidra.app.script.GhidraScript;\n");char[] body=code.toCharArray();int lines=1;
60
+ Matcher im=Pattern.compile("(?m)^\\h*import\\s+(?:static\\s+)?[\\w.$*]+\\s*;").matcher(lex);
61
+ while(im.find()){
62
+ imports.append(code,im.start(),im.end()).append('\n');lines++;
63
+ for(int i=im.start();i<im.end();i++)if(body[i]!='\n'&&body[i]!='\r')body[i]=' ';
64
+ }
65
+ String name="InsightUser_"+attempt.replace("-","");
66
+ return new Source(name,imports+"public class "+name+" extends GhidraScript {\npublic void run() throws Exception {\n"+new String(body)+"\n}\n}\n",lines+2,code.split("\n",-1).length,true);
67
+ }
68
+ static String sha(String s)throws Exception{return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(s.getBytes(StandardCharsets.UTF_8)));}
69
+ static Map<String,Object> execution(String stage,boolean mayRun,Object invoked){
70
+ Map<String,Object> m=new LinkedHashMap<>();m.put("stage",stage);m.put("userCode",mayRun?"may_have_started":"not_started");m.put("entrypointInvoked",invoked);return m;
71
+ }
72
+ static List<Map<String,Object>> diagnostics(List<Diagnostic<? extends JavaFileObject>> all,Source source){
73
+ List<Map<String,Object>> out=new ArrayList<>();
74
+ for(var d:all){
75
+ if(out.size()==MAX_DIAGNOSTICS)break;
76
+ Map<String,Object> v=new LinkedHashMap<>();v.put("severity",d.getKind().name().toLowerCase(Locale.ROOT));v.put("code",d.getCode());
77
+ String message=d.getMessage(Locale.ROOT);v.put("message",message.substring(0,Math.min(message.length(),512)));v.put("messageTruncated",message.length()>512);
78
+ v.put("line",d.getLineNumber()>0?d.getLineNumber():null);v.put("column",d.getColumnNumber()>0?d.getColumnNumber():null);
79
+ long userLine=d.getLineNumber()-source.prefixLines();v.put("sourceLine",userLine>0&&userLine<=source.userLines()?userLine:null);v.put("generatedWrapper",source.wrapped());out.add(v);
80
+ }
81
+ return out;
82
+ }
83
+ static void failure(Map<String,Object> out,String stage,Throwable error,String fallback){
84
+ out.put("success",false);
85
+ Throwable cause=error;
86
+ if(cause instanceof InvocationTargetException e&&e.getTargetException()!=null)cause=e.getTargetException();
87
+ String message=cause==null?fallback:cause.getClass().getSimpleName()+": "+Objects.toString(cause.getMessage(),"");
88
+ out.put("diagnostic",Map.of("kind",stage.equals("compile")?"java-compile":stage.equals("load")?"java-load":"java-"+stage,"message",message.substring(0,Math.min(message.length(),2048)),"messageTruncated",message.length()>2048));
89
+ }
90
+ public static void execute(Path requestFile,GhidraState state,TaskMonitor monitor)throws Exception {
91
+ JsonObject p=JsonParser.parseString(Files.readString(requestFile)).getAsJsonObject();
92
+ Path root=requestFile.toAbsolutePath().getParent();String code=p.get("code").getAsString(),attempt=p.get("attemptId").getAsString();
93
+ if(!sha(code).equals(p.get("sourceSha256").getAsString())||!attempt.matches("[a-f0-9-]{36}"))throw new IllegalArgumentException("Inline attempt binding differs");
94
+ Source source=source(code,attempt);BoundedOutput build=new BoundedOutput(),console=new BoundedOutput();
95
+ Map<String,Object> out=new LinkedHashMap<>();out.put("protocol","insight-inline-v1");out.put("attemptId",attempt);out.put("requestId",p.get("requestId").getAsString());out.put("sourceSha256",sha(code));out.put("success",false);
96
+ String stage="prepare";boolean mayRun=false;Object invoked=false;
97
+ PrintStream originalOut=System.out,originalErr=System.err;
98
+ PrintWriter scriptWriter=new PrintWriter(new OutputStreamWriter(console,StandardCharsets.UTF_8),true);
99
+ try {
100
+ JavaCompiler compiler=ToolProvider.getSystemJavaCompiler();if(compiler==null)throw new IllegalStateException("JDK Java compiler is unavailable");
101
+ Path file=root.resolve("source").resolve(source.name().replace('.','/')+".java"),classes=root.resolve("classes");
102
+ Files.createDirectories(file.getParent());Files.createDirectory(classes);Files.writeString(file,source.text(),StandardOpenOption.CREATE_NEW);
103
+ stage="compile";DiagnosticCollector<JavaFileObject> ds=new DiagnosticCollector<>();
104
+ try(StandardJavaFileManager manager=compiler.getStandardFileManager(ds,Locale.ROOT,StandardCharsets.UTF_8)){
105
+ // Compile only this immutable attempt. No source discovery or annotation/plugin execution.
106
+ List<String> options=List.of("-proc:none","-encoding","UTF-8","-classpath",System.getProperty("java.class.path"),"-sourcepath",root.resolve("source").toString(),"-d",classes.toString());
107
+ boolean ok=Boolean.TRUE.equals(compiler.getTask(new OutputStreamWriter(build,StandardCharsets.UTF_8),manager,ds,options,null,manager.getJavaFileObjects(file)).call());
108
+ out.put("diagnostics",diagnostics(ds.getDiagnostics(),source));out.put("diagnosticsTruncated",ds.getDiagnostics().size()>MAX_DIAGNOSTICS);
109
+ if(!ok){
110
+ PrintWriter messages=new PrintWriter(new OutputStreamWriter(build,StandardCharsets.UTF_8));
111
+ for(var d:ds.getDiagnostics())messages.println("Inline.java:"+d.getLineNumber()+":"+d.getColumnNumber()+": "+d.getKind()+": "+d.getMessage(Locale.ROOT));messages.flush();
112
+ failure(out,stage,null,ds.getDiagnostics().stream().filter(d->d.getKind()==Diagnostic.Kind.ERROR).findFirst().map(d->d.getMessage(Locale.ROOT)).orElse("Java compilation failed"));return;
113
+ }
114
+ }
115
+ stage="load";
116
+ try(URLClassLoader loader=new URLClassLoader(new URL[]{classes.toUri().toURL()},InsightInlineExecutor.class.getClassLoader())){
117
+ // Resolution and reflection are performed without class initialization.
118
+ Class<?> type=Class.forName(source.name(),false,loader);
119
+ if(type.getClassLoader()!=loader)throw new IllegalArgumentException("Source class collides with a parent class");
120
+ if(!GhidraScript.class.isAssignableFrom(type)||Modifier.isAbstract(type.getModifiers())||!Modifier.isPublic(type.getModifiers()))throw new IllegalArgumentException("Inline class must be a public concrete GhidraScript");
121
+ Constructor<?> constructor=type.getDeclaredConstructor();if(!Modifier.isPublic(constructor.getModifiers()))throw new IllegalAccessException("Inline class needs a public default constructor");
122
+ // Set the conservative barrier BEFORE initialization or any caller-controlled method.
123
+ System.setOut(new PrintStream(console,true,StandardCharsets.UTF_8));System.setErr(System.out);
124
+ stage="initialize";mayRun=true;Class.forName(source.name(),true,loader);
125
+ stage="construct";
126
+ GhidraScript script=(GhidraScript)constructor.newInstance();
127
+ script.setSourceFile(new ResourceFile(file.toFile()));
128
+ String args=p.get("args").getAsString().trim();script.setScriptArgs(args.isEmpty()?new String[0]:args.split("\\s+"));
129
+ stage="execute";invoked=null; // Framework setup may fail before run(); never assert run() entry from an exception.
130
+ script.execute(state,monitor,scriptWriter);
131
+ out.put("success",true);
132
+ }
133
+ }catch(Throwable error){failure(out,stage,error,"Inline attempt failed");}
134
+ finally {
135
+ scriptWriter.flush();
136
+ System.setOut(originalOut);System.setErr(originalErr);
137
+ out.put("execution",execution(stage,mayRun,invoked));out.put("compilerOutput",build.value());out.put("console_output",console.text());out.put("outputTruncated",console.truncated);
138
+ Path receipt=root.resolve("receipt.json"),temporary=root.resolve("receipt.new");Files.writeString(temporary,JSON.toJson(out),StandardOpenOption.CREATE_NEW);
139
+ try(FileChannel channel=FileChannel.open(temporary,StandardOpenOption.WRITE)){channel.force(true);}
140
+ Files.move(temporary,receipt,StandardCopyOption.ATOMIC_MOVE);
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,14 @@
1
+ import ghidra.app.script.GhidraScript;
2
+ import insight.InsightInlineExecutor;
3
+ import java.nio.file.Path;
4
+ import java.nio.charset.StandardCharsets;
5
+ import java.util.Base64;
6
+
7
+ /** Only fixed trusted code is passed to the upstream script provider. User source is an input file. */
8
+ public class InsightInlineScript extends GhidraScript {
9
+ public void run() throws Exception {
10
+ Path request=Path.of(new String(Base64.getDecoder().decode(getScriptArgs()[0]),StandardCharsets.UTF_8));
11
+ InsightInlineExecutor.execute(request,getState(),monitor);
12
+ println("INSIGHT_INLINE_COMPLETE");
13
+ }
14
+ }
@@ -0,0 +1,92 @@
1
+ package com.eyeauras.insight;
2
+
3
+ import java.math.BigInteger;
4
+ import java.util.*;
5
+ import java.util.function.LongSupplier;
6
+
7
+ /** Current mapped PE32+/AMD64 runtime regions, never semantic function reconstruction. */
8
+ public final class InsightPeRanges {
9
+ public record Span(BigInteger begin,BigInteger endExclusive,boolean execute) {}
10
+ public interface Bytes { int read(BigInteger at,byte[] out)throws Exception; }
11
+ public record Range(BigInteger begin,BigInteger endExclusive,BigInteger recordAddress) {
12
+ public Map<String,Object> facts(){return Map.of("source","pe-exception-directory","begin",begin.toString(16),"endExclusive",endExclusive.toString(16),"tableRecordAddress",recordAddress.toString(16),"semanticMembership","not-established","instructionBoundary","not-checked","unwindInfo","not-parsed");}
13
+ }
14
+ public record Result(Map<String,Object> lookup,List<Range> ranges) {}
15
+ private static final BigInteger U64=BigInteger.ONE.shiftLeft(64);
16
+ private final List<BigInteger> addresses;
17
+ private final BigInteger base;private final List<Span> spans;private final Bytes memory;private final LongSupplier clock;private final int maximum;private final long start;
18
+ private int bytesRead=0,recordsValidated=0;private long recordCount=0;
19
+ private static final class Stop extends Exception {final String state,reason;Stop(String s,String r){state=s;reason=r;}}
20
+ public InsightPeRanges(BigInteger base,List<Span> spans,Bytes memory,int maximum,LongSupplier clock,List<BigInteger> addresses){
21
+ this(base,spans,memory,maximum,clock,addresses,clock.getAsLong());
22
+ }
23
+ public InsightPeRanges(BigInteger base,List<Span> spans,Bytes memory,int maximum,LongSupplier clock,List<BigInteger> addresses,long started){
24
+ if(base.signum()<0||base.compareTo(U64)>=0||maximum<1||maximum>16777216)throw new IllegalArgumentException("Invalid PE lookup bounds");
25
+ if(addresses.size()>20)throw new IllegalArgumentException("At most 20 addresses");this.addresses=List.copyOf(addresses);
26
+ start=started;this.base=base;this.spans=spans.stream().sorted(Comparator.comparing(Span::begin)).toList();this.memory=memory;this.maximum=maximum;this.clock=clock;
27
+ }
28
+ private void tick()throws Stop {if(clock.getAsLong()-start>=5_000_000_000L)throw new Stop("incomplete","timeout");}
29
+ private boolean mapped(BigInteger at,long length,boolean execute)throws Stop {
30
+ BigInteger end=at.add(BigInteger.valueOf(length));if(length<1||at.signum()<0||end.compareTo(U64)>0)return false;
31
+ BigInteger cursor=at;
32
+ for(Span s:spans){tick();if(s.endExclusive.compareTo(cursor)<=0)continue;if(s.begin.compareTo(cursor)>0)return false;if(execute&&!s.execute)return false;cursor=s.endExclusive.min(end);if(cursor.equals(end))return true;}
33
+ return false;
34
+ }
35
+ private byte[] read(BigInteger at,int length,String missing)throws Exception {
36
+ tick();if(length>maximum-bytesRead)throw new Stop("incomplete","byte_limit");
37
+ if(!mapped(at,length,false))throw new Stop("unavailable",missing);
38
+ byte[] out=new byte[length];int got;
39
+ try{got=memory.read(at,out);}catch(Exception e){throw new Stop("unavailable","memory_read_failed");}
40
+ // A failed/short read never establishes absence. Charge every reported physical byte.
41
+ if(got<0||got>length)throw new IllegalStateException("Invalid native byte count");bytesRead+=got;
42
+ if(got!=length)throw new Stop("unavailable","short_read");tick();return out;
43
+ }
44
+ private static int u16(byte[] a,int i){return (a[i]&255)|((a[i+1]&255)<<8);}
45
+ private static long u32(byte[] a,int i){return Integer.toUnsignedLong((a[i]&255)|((a[i+1]&255)<<8)|((a[i+2]&255)<<16)|((a[i+3]&255)<<24));}
46
+ private void valid(boolean condition,String reason)throws Stop {if(!condition)throw new Stop("malformed",reason);}
47
+ private BigInteger rva(long offset,long size)throws Stop {valid(offset>0&&size>0&&offset+size<=0x1_0000_0000L,"invalid_rva");BigInteger at=base.add(BigInteger.valueOf(offset));valid(at.add(BigInteger.valueOf(size)).compareTo(U64)<=0,"address_overflow");return at;}
48
+ public Result run()throws Exception {
49
+ List<Range> ranges=new ArrayList<>();String state="validated",reason="validated";
50
+ try{
51
+ byte[] dos=read(base,64,"headers_not_mapped");valid(u16(dos,0)==0x5a4d,"invalid_dos_signature");long pe=u32(dos,60);valid(pe>=64,"invalid_pe_offset");
52
+ byte[] header=read(base.add(BigInteger.valueOf(pe)),24,"headers_not_mapped");valid(u32(header,0)==0x4550,"invalid_pe_signature");
53
+ if(u16(header,4)!=0x8664)throw new Stop("unavailable","unsupported_machine");
54
+ int optionalSize=u16(header,20);valid(optionalSize>=112,"short_optional_header");
55
+ byte[] optional=read(base.add(BigInteger.valueOf(pe+24)),112,"headers_not_mapped");
56
+ if(u16(optional,0)!=0x20b)throw new Stop("unavailable","unsupported_optional_header");
57
+ long imageSize=u32(optional,56),headerSize=u32(optional,60),directories=u32(optional,108);
58
+ valid(imageSize>0&&headerSize>=pe+24+optionalSize&&headerSize<=imageSize,"invalid_image_headers");
59
+ valid(directories<=(optionalSize-112)/8,"short_optional_header");
60
+ valid(base.add(BigInteger.valueOf(imageSize)).compareTo(U64)<=0,"address_overflow");
61
+ if(!mapped(base,pe+24+optionalSize,false))throw new Stop("unavailable","headers_not_mapped");
62
+ if(directories<4){state="absent";reason="no_exception_directory";}
63
+ else{
64
+ byte[] directory=read(base.add(BigInteger.valueOf(pe+24+136)),8,"headers_not_mapped");
65
+ long table=u32(directory,0),size=u32(directory,4);
66
+ if(table==0&&size==0){state="absent";reason="no_exception_directory";}
67
+ else{
68
+ valid(table>0&&size>0&&size%12==0&&table%4==0&&table+size<=imageSize,"invalid_exception_directory");
69
+ recordCount=size/12;BigInteger tableAt=rva(table,size);long previousEnd=0;
70
+ for(long offset=0;offset<size;){
71
+ tick();int length=(int)Math.min(4092,size-offset);byte[] chunk=read(tableAt.add(BigInteger.valueOf(offset)),length,"table_not_mapped");
72
+ for(int i=0;i<length;i+=12){
73
+ tick();long begin=u32(chunk,i),end=u32(chunk,i+4),unwind=u32(chunk,i+8);
74
+ valid(begin>0&&begin<end&&end<=imageSize,"invalid_runtime_range");
75
+ valid(begin>=previousEnd,"unordered_or_overlapping_ranges");
76
+ valid(unwind>0&&unwind<imageSize&&unwind%4==0,"invalid_unwind_rva");
77
+ BigInteger lo=rva(begin,end-begin);
78
+ if(!mapped(lo,end-begin,true))throw new Stop("unavailable","runtime_range_not_initialized_executable");
79
+ if(!mapped(rva(unwind,1),1,false))throw new Stop("unavailable","unwind_not_mapped");
80
+ BigInteger hi=base.add(BigInteger.valueOf(end));
81
+ if(addresses.stream().anyMatch(at->at.compareTo(lo)>=0&&at.compareTo(hi)<0))ranges.add(new Range(lo,hi,tableAt.add(BigInteger.valueOf(offset+i))));previousEnd=end;recordsValidated++;
82
+ }
83
+ offset+=length;
84
+ }
85
+ }
86
+ }
87
+ tick();
88
+ }catch(Stop e){state=e.state;reason=e.reason;ranges.clear();}
89
+ boolean complete=state.equals("validated")||state.equals("absent");
90
+ return new Result(Map.of("state",state,"complete",complete,"reason",reason,"headersSource","current-mapped-memory","recordCount",recordCount,"recordsValidated",recordsValidated,"bytesRead",bytesRead,"maximumBytes",maximum,"deadlineMilliseconds",5000),List.copyOf(ranges));
91
+ }
92
+ }
@@ -111,6 +111,27 @@ public class InsightResearchQueries extends GhidraScript {
111
111
  candidates.add(map("address",currentProgram.getImageBase().toString(),"name","image_base","kind","image_base_candidate","confirmed",false,"provenance","program.image-base"));
112
112
  return map("entry_points",confirmed,"count",confirmed.size(),"candidates",candidates,"scan",scan(),"truncated",stopped,"semantics",map("kind","metadata","limitation","External-entry designation is Ghidra metadata, not proof of OS startup behavior; candidates are not confirmed entries."));
113
113
  }
114
+ Object navigation()throws Exception{
115
+ final int cap=128;int functions=0,oneByte=0,entryInstructions=0,instructions=0,references=0,data=0,strings=0;long bodyBytes=0;
116
+ var fs=currentProgram.getFunctionManager().getFunctions(true);
117
+ while(fs.hasNext()&&functions<cap){if(!visit())break;var f=fs.next();functions++;long size=f.getBody().getNumAddresses();bodyBytes+=size;if(size==1)oneByte++;if(currentProgram.getListing().getInstructionAt(f.getEntryPoint())!=null)entryInstructions++;}
118
+ boolean functionsComplete=!fs.hasNext();
119
+ var is=currentProgram.getListing().getInstructions(true);
120
+ while(is.hasNext()&&instructions<cap){if(!visit())break;is.next();instructions++;}boolean instructionsComplete=!is.hasNext();
121
+ var rs=currentProgram.getReferenceManager().getReferenceIterator(currentProgram.getMinAddress());
122
+ while(rs.hasNext()&&references<cap){if(!visit())break;rs.next();references++;}boolean referencesComplete=!rs.hasNext();
123
+ var ds=currentProgram.getListing().getDefinedData(true);
124
+ while(ds.hasNext()&&data<cap){if(!visit())break;var d=ds.next();data++;if(d.hasStringValue())strings++;}boolean dataComplete=!ds.hasNext();
125
+ boolean complete=functionsComplete&&instructionsComplete&&referencesComplete&&dataComplete;
126
+ String assessment=functions>0&&oneByte*5>=functions*4&&(functions-entryInstructions)*5>=functions*4?"sparse-metadata":functions==0&&instructions==0?"no-code-observed":"observed-metadata";
127
+ return map("functions",map("sampled",functions,"oneByteBodies",oneByte,"bodyBytes",bodyBytes,"entriesWithInstructions",entryInstructions,"complete",functionsComplete),
128
+ "instructions",map("sampled",instructions,"complete",instructionsComplete),"references",map("sampled",references,"complete",referencesComplete),
129
+ "definedData",map("sampled",data,"strings",strings,"complete",dataComplete),
130
+ "scan",map("visited",visited,"maximum",maxScan,"sampleLimitPerCategory",cap,"complete",complete,"reason",complete?"exhausted":stopped?"budget":"sample_limit"),
131
+ "assessment",assessment,"assessmentAlgorithm","one-byte-and-missing-entry-instruction-ratio-at-least-0.8-v1",
132
+ "recommendations",assessment.equals("sparse-metadata")||assessment.equals("no-code-observed")?List.of("Treat missing stored xrefs, strings and function containment as unknown. For known function entries try functions inspect --fields decompiled_code,callees; refresh stale/failed output with functions force-decompile --function <entry>.","For executable bytes without a function entry use functions decode. These reads do not repair Listing instructions or stored xrefs.","Find undefined literals with analysis search-byte-patterns and its next_address continuation; strings search visits defined data only.","Consider an explicit bounded analysis/disassembly Update only when needed; this diagnostic does not run analyzers."):List.of("Use observed metadata for navigation; verify individual bodies and references for the question under study."),
133
+ "coverage","At most 128 inputs per category, in address order, sharing max-scan and a five-second deadline. References are stored DB references; strings count only sampled defined data. Observations and heuristics do not prove analysis or semantic completeness.");
134
+ }
114
135
  Object strings(boolean search)throws Exception{
115
136
  List<Object> rows=new PagedRows();String needle=str(search?"search_term":"filter","");Pattern regex=search?safeRegex(needle):null;String encoding=str("encoding","");int min=number("min_length",search?4:0,0,65536);var iter=currentProgram.getListing().getDefinedData(true);
116
137
  while(iter.hasNext()){if(!visit())break;var d=iter.next();if(!d.hasStringValue())continue;if(d.getLength()>65536){stopped=true;break;}String value=Objects.toString(d.getValue(),"");if(value.length()>8192){stopped=true;break;}String enc=d.getDataType().getName().toLowerCase(Locale.ROOT).contains("unicode")?"unicode":"ascii";
@@ -203,7 +224,7 @@ public class InsightResearchQueries extends GhidraScript {
203
224
  if(consumed==input.length&&available<maximum&&reason.equals("byte_limit"))reason="executable_block_end";
204
225
  return map("address",start.toString(),"language",language,"compiler",currentProgram.getCompilerSpec().getCompilerSpecID().toString(),"input_bytes",HexFormat.of().formatHex(input),"input_sha256",HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256").digest(input)),"maximum_bytes",maximum,"maximum_instructions",maxInstructions,"instructions",rows,"consumed_bytes",consumed,"stop_reason",reason,"truncated",List.of("byte_limit","instruction_limit","truncated_instruction","executable_block_end").contains(reason),"semantics","Linear pseudo instructions in the selected program context; direct targets are computed from bytes, not function membership or stored xrefs.");
205
226
  }
206
- Object execute(String operation)throws Exception{return switch(operation){case "/research_view"->map("modification",Long.toString(currentProgram.getModificationNumber()),"language",currentProgram.getLanguageID().toString(),"compiler",currentProgram.getCompilerSpec().getCompilerSpecID().toString());case "/decode_instructions"->decodeInstructions();case "/find_dead_code"->dead();case "/detect_crypto_constants"->bytes(true);case "/search_byte_patterns"->bytes(false);case "/get_valid_data_types"->types(true);case "/find_data_types"->types(false);case "/validate_data_type"->validateType();case "/validate_function_prototype"->validatePrototype();case "/get_entry_points"->entries();case "/list_globals"->globals();case "/list_strings"->strings(false);case "/search_strings"->strings(true);case "/search_instructions"->instructions();default->throw new IllegalArgumentException("Unknown native query");};}
227
+ Object execute(String operation)throws Exception{return switch(operation){case "/navigation_diagnostic"->navigation();case "/research_view"->map("modification",Long.toString(currentProgram.getModificationNumber()),"language",currentProgram.getLanguageID().toString(),"compiler",currentProgram.getCompilerSpec().getCompilerSpecID().toString());case "/decode_instructions"->decodeInstructions();case "/find_dead_code"->dead();case "/detect_crypto_constants"->bytes(true);case "/search_byte_patterns"->bytes(false);case "/get_valid_data_types"->types(true);case "/find_data_types"->types(false);case "/validate_data_type"->validateType();case "/validate_function_prototype"->validatePrototype();case "/get_entry_points"->entries();case "/list_globals"->globals();case "/list_strings"->strings(false);case "/search_strings"->strings(true);case "/search_instructions"->instructions();default->throw new IllegalArgumentException("Unknown native query");};}
207
228
  public void run()throws Exception{
208
229
  var request=JsonParser.parseString(new String(Base64.getDecoder().decode(getScriptArgs()[0]),StandardCharsets.UTF_8)).getAsJsonObject();p=request.getAsJsonObject("parameters");maxScan=number("max_scan",100000,1,200000);limit=number("limit",100,1,1000);offset=number("offset",0,0,2000000);deadline=System.nanoTime()+number("timeout_seconds",5,1,30)*1000000000L;
209
230
  boundedMonitor=TimeoutTaskMonitor.timeoutIn(number("timeout_seconds",5,1,30),TimeUnit.SECONDS,monitor);
@@ -0,0 +1,69 @@
1
+ package com.eyeauras.insight;
2
+
3
+ import com.xebyte.core.*;
4
+ import ghidra.program.model.listing.Program;
5
+ import ghidra.program.model.listing.Function;
6
+ import ghidra.program.model.address.Address;
7
+ import java.math.BigInteger;
8
+ import java.util.LinkedHashMap;
9
+ import java.util.Map;
10
+
11
+ /** Precompiled, authenticated bridge read. Does not open, analyze, transact or save programs. */
12
+ public final class InsightResearchView {
13
+ private final ProgramProvider programs;
14
+ private final ThreadingStrategy threading;
15
+ public InsightResearchView(ProgramProvider programs,ThreadingStrategy threading){this.programs=programs;this.threading=threading;}
16
+ private Program openProgram(String path){
17
+ if(programs==null||path==null||!path.startsWith("/")||path.length()>4096)throw new IllegalArgumentException("An explicit open program path is required");
18
+ Program selected=null;
19
+ for(Program p:programs.getAllOpenPrograms())if(p!=null&&!p.isClosed()&&p.getDomainFile()!=null&&path.equals(p.getDomainFile().getPathname())){
20
+ if(selected!=null&&selected!=p)throw new IllegalStateException("Program path is ambiguous");selected=p;
21
+ }
22
+ if(selected==null)throw new IllegalArgumentException("Program is not open");return selected;
23
+ }
24
+ @McpTool(path="/insight/research-view/v1",method="GET",description="Read modification/language/compiler of an already open program",access=ToolAccess.READ_ONLY)
25
+ public Response view(@Param(value="program",source=ParamSource.QUERY,allowEmpty=false) String path)throws Exception {
26
+ return threading.executeRead(()->{
27
+ Program selected=openProgram(path);
28
+ return Response.ok(Map.of("protocol","insight-research-view-v1","modification",Long.toString(selected.getModificationNumber()),"language",selected.getLanguageID().toString(),"compiler",selected.getCompilerSpec().getCompilerSpecID().toString()));
29
+ });
30
+ }
31
+ private static Map<String,Object> miss(String reason){return Map.of("status","not_found","reason",reason);}
32
+ private static Map<String,Object> resolved(Function function,Address at,String kind){
33
+ Map<String,Object> row=new LinkedHashMap<>();row.put("status","resolved");row.put("entryPoint",Long.toUnsignedString(function.getEntryPoint().getOffset(),16));
34
+ row.put("kind",kind);row.put("recordedBodyContainsAddress",function.getBody().contains(at));
35
+ if(!kind.equals("recorded-containing"))row.put("distanceBytes",at.subtract(function.getEntryPoint()));return row;
36
+ }
37
+ @McpTool(path="/insight/function-entries/v1",method="GET",description="Resolve up to 20 addresses using recorded bodies or a bounded preceding-entry candidate; no inferred membership",access=ToolAccess.READ_ONLY)
38
+ public Response entries(@Param(value="program",source=ParamSource.QUERY,allowEmpty=false) String path,
39
+ @Param(value="addresses",source=ParamSource.QUERY,allowEmpty=false) String addresses,
40
+ @Param(value="max_entry_distance",source=ParamSource.QUERY,allowEmpty=false) int maximum)throws Exception {
41
+ if(maximum<1||maximum>65536||addresses==null||addresses.length()>339)throw new IllegalArgumentException("Expected 1–20 addresses and a distance of 1–65536 bytes");
42
+ String[] words=addresses.split(",",-1);if(words.length<1||words.length>20)throw new IllegalArgumentException("Expected 1–20 addresses");
43
+ for(String word:words)if(!word.matches("[a-f0-9]{1,16}"))throw new IllegalArgumentException("Expected canonical default-space hexadecimal addresses");
44
+ return threading.executeRead(()->{
45
+ Program program=openProgram(path);long modification=program.getModificationNumber();var functions=program.getFunctionManager();
46
+ var space=program.getAddressFactory().getDefaultAddressSpace();var memory=program.getMemory();Map<String,Object> items=new LinkedHashMap<>();
47
+ for(String word:words){
48
+ if(items.containsKey(word)||!new BigInteger(word,16).toString(16).equals(word))throw new IllegalArgumentException("Expected distinct canonical addresses");
49
+ if(new BigInteger(word,16).bitLength()>space.getSize()){items.put(word,miss("address_out_of_range"));continue;}
50
+ Address at=space.getAddress(Long.parseUnsignedLong(word,16));
51
+ Function function=functions.getFunctionAt(at);
52
+ if(function!=null){items.put(word,resolved(function,at,"exact-entry"));continue;}
53
+ function=functions.getFunctionContaining(at);
54
+ if(function!=null){items.put(word,resolved(function,at,"recorded-containing"));continue;}
55
+ var block=memory.getBlock(at);
56
+ if(block==null||!block.isInitialized()||!block.isExecute()){items.put(word,miss("address_not_in_initialized_executable_block"));continue;}
57
+ // Address-seeded indexed iterator; consume at most one previous entry, never a full-table scan.
58
+ var previous=functions.getFunctions(at,false);
59
+ if(!previous.hasNext()){items.put(word,miss("no_preceding_entry"));continue;}
60
+ function=previous.next();Address entry=function.getEntryPoint();
61
+ if(!entry.getAddressSpace().equals(space)||entry.compareTo(at)>=0||!block.contains(entry)){items.put(word,miss("entry_outside_block"));continue;}
62
+ if(new BigInteger(word,16).subtract(new BigInteger(Long.toUnsignedString(entry.getOffset(),16),16)).compareTo(BigInteger.valueOf(maximum))>0){items.put(word,miss("entry_distance_exceeded"));continue;}
63
+ items.put(word,resolved(function,at,"preceding-entry-candidate"));
64
+ }
65
+ if(modification!=program.getModificationNumber())throw new IllegalStateException("Program changed during function-entry lookup");
66
+ return Response.ok(Map.of("protocol","insight-function-entries-v1","modification",Long.toString(modification),"items",items));
67
+ });
68
+ }
69
+ }
@@ -0,0 +1,49 @@
1
+ package com.eyeauras.insight;
2
+
3
+ import com.xebyte.core.*;
4
+ import com.google.gson.Gson;
5
+ import com.google.gson.GsonBuilder;
6
+ import ghidra.program.model.listing.Program;
7
+ import java.math.BigInteger;
8
+ import java.util.*;
9
+
10
+ /** Authenticated precompiled read of an already-open byte-addressed default space. */
11
+ public final class InsightStringAnchors {
12
+ // Upstream Response.ok omits null map values. This protocol requires explicit nullable facts.
13
+ private static final Gson JSON=new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
14
+ private final ProgramProvider programs;private final ThreadingStrategy threading;
15
+ public InsightStringAnchors(ProgramProvider programs,ThreadingStrategy threading){this.programs=programs;this.threading=threading;}
16
+ private static BigInteger address(String value){if(value==null||!value.matches("0|[1-9a-f][a-f0-9]{0,15}"))throw new IllegalArgumentException("Expected canonical default-space address");return new BigInteger(value,16);}
17
+ @McpTool(path="/insight/string-anchors/v1",method="GET",description="Bounded literal byte matches with separate NUL-delimited candidate context",access=ToolAccess.READ_ONLY)
18
+ public Response anchors(@Param(value="program",source=ParamSource.QUERY,allowEmpty=false) String path,
19
+ @Param(value="text",source=ParamSource.QUERY,allowEmpty=false) String text,
20
+ @Param(value="encodings",source=ParamSource.QUERY,allowEmpty=false) String encodings,
21
+ @Param(value="scan_from",source=ParamSource.QUERY,allowEmpty=false) String start,
22
+ @Param(value="scan_through",source=ParamSource.QUERY,allowEmpty=false) String end,
23
+ @Param(value="resume_address",source=ParamSource.QUERY,allowEmpty=false) String resume,
24
+ @Param(value="resume_encoding",source=ParamSource.QUERY,allowEmpty=false) String encoding,
25
+ @Param(value="max_string_bytes",source=ParamSource.QUERY,allowEmpty=false) int cap,
26
+ @Param(value="max_bytes",source=ParamSource.QUERY,allowEmpty=false) int bytes,
27
+ @Param(value="max_candidates",source=ParamSource.QUERY,allowEmpty=false) int candidates,
28
+ @Param(value="timeout_seconds",source=ParamSource.QUERY,allowEmpty=false) int seconds,
29
+ @Param(value="limit",source=ParamSource.QUERY,allowEmpty=false) int limit)throws Exception{
30
+ var config=new InsightAnchorScan.Config(text,Arrays.asList(encodings.split(",",-1)),address(start),address(end),address(resume),encoding,cap,bytes,candidates,seconds,limit);
31
+ return threading.executeRead(()->{
32
+ if(programs==null||path==null||!path.startsWith("/")||path.length()>4096)throw new IllegalArgumentException("Explicit open program path required");
33
+ Program program=null;for(Program p:programs.getAllOpenPrograms())if(p!=null&&!p.isClosed()&&p.getDomainFile()!=null&&path.equals(p.getDomainFile().getPathname())){if(program!=null&&program!=p)throw new IllegalStateException("Ambiguous program path");program=p;}
34
+ if(program==null)throw new IllegalArgumentException("Program is not open");
35
+ var space=program.getAddressFactory().getDefaultAddressSpace();if(space.getAddressableUnitSize()!=1)throw new IllegalArgumentException("Only byte-addressed default space is supported");
36
+ if(config.end().bitLength()>space.getSize())throw new IllegalArgumentException("Range outside default address space");
37
+ long modification=program.getModificationNumber();var memory=program.getMemory();List<InsightAnchorScan.Span> raw=new ArrayList<>();
38
+ for(var block:memory.getBlocks())if(block.isInitialized()&&block.getStart().getAddressSpace().equals(space)){
39
+ var lo=new BigInteger(Long.toUnsignedString(block.getStart().getOffset(),16),16).max(config.start());var hi=new BigInteger(Long.toUnsignedString(block.getEnd().getOffset(),16),16).min(config.end());
40
+ if(lo.compareTo(hi)<=0)raw.add(new InsightAnchorScan.Span(lo,hi));
41
+ }
42
+ raw.sort(Comparator.comparing(InsightAnchorScan.Span::start));List<InsightAnchorScan.Span> spans=new ArrayList<>();
43
+ for(var s:raw){if(!spans.isEmpty()&&s.start().equals(spans.getLast().end().add(BigInteger.ONE))){var previous=spans.removeLast();spans.add(new InsightAnchorScan.Span(previous.start(),s.end()));}else spans.add(s);}
44
+ Map<String,Object> value=new LinkedHashMap<>(new InsightAnchorScan(config,spans,(at,out)->memory.getBytes(space.getAddress(at.longValue()),out),System::nanoTime).run());
45
+ if(program.getModificationNumber()!=modification)throw new IllegalStateException("Program changed during string-anchor scan");
46
+ value.put("modification",Long.toString(modification));return Response.text(JSON.toJson(value));
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,125 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { StringDecoder } from 'node:string_decoder';
3
+ export class InsightCommandError extends Error {
4
+ rpc;
5
+ constructor(rpc) {
6
+ super(rpc.message);
7
+ this.rpc = rpc;
8
+ this.name = 'InsightCommandError';
9
+ }
10
+ }
11
+ export class InsightTransportError extends Error {
12
+ dispatched;
13
+ constructor(message, dispatched) {
14
+ super(message);
15
+ this.dispatched = dispatched;
16
+ this.name = 'InsightTransportError';
17
+ }
18
+ }
19
+ /** A thin client of Factory's existing stream, with no reconnect or command replay. */
20
+ export async function connectInsight(options) {
21
+ const child = spawn(options.executable, [...(options.arguments ?? []), '--json-rpc'], { cwd: options.cwd, env: options.env, stdio: ['pipe', 'pipe', 'pipe'], shell: false });
22
+ const maximum = 4 * 1024 ** 2, decoder = new StringDecoder('utf8');
23
+ let buffer = '', bytes = 0, sequence = 0, accepting = true, failed = false, ended = false;
24
+ let closePromise, tail = Promise.resolve();
25
+ const pending = new Map();
26
+ // Drain diagnostics without retaining arbitrary stderr, paths, tokens or unbounded output.
27
+ child.stderr.resume();
28
+ const fail = (message) => {
29
+ if (failed)
30
+ return;
31
+ failed = true;
32
+ accepting = false;
33
+ for (const p of pending.values())
34
+ p.reject(new InsightTransportError(message, true));
35
+ pending.clear();
36
+ child.stdin.destroy();
37
+ child.stdout.destroy();
38
+ };
39
+ child.on('error', () => fail('Persistent CLI process failed.'));
40
+ child.stdout.on('error', () => fail('Persistent CLI output was lost.'));
41
+ child.stderr.on('error', () => fail('Persistent CLI diagnostic stream was lost.'));
42
+ child.stdin.on('error', () => fail('Persistent CLI input was lost; inspect request/status before retrying a changing command.'));
43
+ const exited = new Promise((resolve, reject) => {
44
+ child.once('close', (code, signal) => {
45
+ ended = true;
46
+ if (code !== 0 || signal || pending.size || buffer.trim()) {
47
+ fail('Persistent CLI closed without a complete acknowledged response.');
48
+ reject(new InsightTransportError('Persistent CLI transport ended unexpectedly.', false));
49
+ }
50
+ else if (failed)
51
+ reject(new InsightTransportError('Persistent CLI transport failed.', false));
52
+ else
53
+ resolve();
54
+ });
55
+ });
56
+ void exited.catch(() => { });
57
+ child.stdout.on('data', (chunk) => {
58
+ if (failed)
59
+ return;
60
+ buffer += decoder.write(chunk);
61
+ bytes += chunk.length;
62
+ if (bytes > maximum) {
63
+ fail('Persistent CLI response exceeded the bounded transport limit.');
64
+ return;
65
+ }
66
+ for (;;) {
67
+ const newline = buffer.indexOf('\n');
68
+ if (newline < 0)
69
+ break;
70
+ const line = buffer.slice(0, newline);
71
+ buffer = buffer.slice(newline + 1);
72
+ bytes = Buffer.byteLength(buffer);
73
+ if (!line.trim())
74
+ continue;
75
+ try {
76
+ const r = JSON.parse(line);
77
+ if (!r || r.jsonrpc !== '2.0' || typeof r.id !== 'number' || !pending.has(r.id) || Object.hasOwn(r, 'result') === Object.hasOwn(r, 'error'))
78
+ throw Error();
79
+ const p = pending.get(r.id);
80
+ if (Object.hasOwn(r, 'error') && (!r.error || typeof r.error !== 'object' || typeof r.error.code !== 'number' || !Number.isInteger(r.error.code) || typeof r.error.message !== 'string'))
81
+ throw Error();
82
+ pending.delete(r.id);
83
+ if (r.error)
84
+ p.reject(new InsightCommandError(r.error));
85
+ else
86
+ p.resolve(r.result);
87
+ }
88
+ catch {
89
+ fail('Persistent CLI returned a malformed or uncorrelated response.');
90
+ return;
91
+ }
92
+ }
93
+ });
94
+ await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', () => reject(new InsightTransportError('Could not start persistent CLI.', false))); });
95
+ return {
96
+ execute(argv) {
97
+ if (!Array.isArray(argv) || argv.length > 512 || argv.some(x => typeof x !== 'string' || x.includes('\0')) || Buffer.byteLength(JSON.stringify(argv)) > 1024 ** 2)
98
+ return Promise.reject(new TypeError('argv must be a bounded array of strings without NUL.'));
99
+ if (!accepting || failed || ended)
100
+ return Promise.reject(new InsightTransportError('Persistent CLI connection is closed.', false));
101
+ const args = [...argv];
102
+ const request = tail.then(() => {
103
+ if (failed || ended)
104
+ throw new InsightTransportError('Persistent CLI connection was lost before dispatch.', false);
105
+ const id = ++sequence;
106
+ return new Promise((resolve, reject) => {
107
+ pending.set(id, { resolve, reject });
108
+ try {
109
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method: 'cli.execute', params: { argv: args } }) + '\n');
110
+ }
111
+ catch {
112
+ fail('Persistent CLI input was lost; inspect request/status before retrying a changing command.');
113
+ }
114
+ });
115
+ });
116
+ tail = request.catch(() => { });
117
+ return request;
118
+ },
119
+ close() {
120
+ accepting = false;
121
+ return closePromise ??= tail.then(async () => { if (!failed && !ended)
122
+ child.stdin.end(); await exited; });
123
+ }
124
+ };
125
+ }
@@ -0,0 +1,50 @@
1
+ # Persistent research calls
2
+
3
+ For a sequence of CLI research calls, keep one `insight-cli --json-rpc` frontend alive in the
4
+ agent's long-lived JavaScript runtime. The Factory owner and Ghidra processes have their own
5
+ lifetime; retaining only the owner still makes a new frontend validate its installed build.
6
+
7
+ The package exports a small stdio client, independent of the facade's eager imports:
8
+
9
+ ```js
10
+ import {connectInsight, InsightCommandError, InsightTransportError}
11
+ from '@eyeauras/insight-cli/persistent';
12
+
13
+ const cli = await connectInsight({executable: 'insight-cli', cwd: projectDirectory});
14
+ const result = await cli.execute([
15
+ 'ghidra', 'functions', 'inspect', '--session', session, '--target', target,
16
+ '--function', 'main', '--fields', 'entry_point', '--include-call-context', 'false'
17
+ ]);
18
+ // Keep cli for subsequent calls in this investigation.
19
+ ```
20
+
21
+ When package-name resolution is unavailable in the agent host, dynamically import the
22
+ [`persistent-client.mjs`](persistent-client.mjs) beside this reference by its absolute file URL.
23
+ It is generated from the same canonical package module. In a persistent REPL retain `cli` as a
24
+ binding across tool invocations. A new Node process per query does not retain this connection.
25
+ For an explicit installed bin, use `{executable: process.execPath, arguments: [installedBin], cwd}`.
26
+ `env` optionally supplies the child environment. There is no shell or separate profile/config.
27
+ Use existing `--profile` argv flags when selecting a profile explicitly.
28
+
29
+ `execute(argv)` returns the CLI result unchanged, without a JSON-RPC envelope. Calls are queued
30
+ in admission order. Native work can be long: await the original promise through the agent host's
31
+ existing wait facility. This V1 client deliberately has no per-request timeout or automatic
32
+ reconnect/retry. Factory permissions, command validation, IDs and native budgets remain authoritative.
33
+
34
+ * `InsightCommandError.rpc` preserves the JSON-RPC error code, message and data, including typed
35
+ CLI error results. A command failure leaves a healthy connection usable for the next command.
36
+ * `InsightTransportError.dispatched === true` means the request was handed to the stream and its
37
+ acknowledgment is unavailable; it does not prove execution. Changing commands require their
38
+ existing request-ID/status recovery. The client never resends them.
39
+ * `dispatched === false` means this client did not send that request, including queued requests
40
+ after a transport failure. Reconnection is an explicit new connection, not an implicit replay.
41
+
42
+ The client bounds each response line to four MiB, validates IDs and envelopes, and fails the
43
+ connection on malformed, uncorrelated or oversized replies. It drains stderr without retaining
44
+ arbitrary native diagnostics in memory. Read command-level diagnostics from their typed results
45
+ and the existing profile diagnostic locations.
46
+
47
+ Finish owned Ghidra contexts with the normal scoped commands, following the project's shared
48
+ server policy. Then `await cli.close()` drains already admitted requests and ends this stdio
49
+ connection. It does not issue `ipc-server stop`, close another agent's context or stop a server.
50
+ Do not call close while expecting to admit more requests.
@@ -51,6 +51,18 @@ finding. Narrow the query or raise its budget within the declared bound. Oversiz
51
51
  also stop a scan rather than silently claiming complete coverage. Returned string values are
52
52
  bounded; check truncation fields.
53
53
 
54
+ `strings list` and `strings search` visit **defined data**, not every possible literal in memory.
55
+ The visited-input budget includes non-string data. Zero matches with an incomplete scan means
56
+ the searched prefix contains no matches; it does not establish absence. Increasing `--offset`
57
+ only skips matching rows from that same scan and cannot reach a later unscanned string. These
58
+ scans have no continuation cursor. Increase `--max-scan` up to 200000 when that covers the
59
+ needed data. A narrower text filter alone does not skip earlier visited data or reach beyond
60
+ that bound; use an explicitly addressed memory search when the needed region is known.
61
+ For undefined ASCII/UTF-16 literals, encode the expected bytes and use
62
+ `analysis search-byte-patterns --pattern <hex> --max-bytes <bytes>`; that separate initialized-
63
+ memory search exposes `--scan-from`/`next_address` continuation. Byte matches still need
64
+ interpretation and do not define string data or prove a stored xref exists.
65
+
54
66
  Bounded string/instruction regex search supports literals, escapes, anchors, character classes
55
67
  and at most one `.*` wildcard. Branching/nested repetition and unsupported patterns are rejected.
56
68
  Use the literal `strings search --contains` convenience when no regex is needed.