@pensar/apex 0.0.27 → 0.0.28-canary.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.
@@ -41485,13 +41485,170 @@ Remember: You are a focused penetration testing agent assigned a specific target
41485
41485
  import { exec } from "child_process";
41486
41486
  import { promisify } from "util";
41487
41487
  import {
41488
- writeFileSync,
41488
+ writeFileSync as writeFileSync2,
41489
41489
  appendFileSync,
41490
- readdirSync,
41490
+ readdirSync as readdirSync2,
41491
+ readFileSync as readFileSync2,
41492
+ existsSync as existsSync2
41493
+ } from "fs";
41494
+ import { join as join2 } from "path";
41495
+
41496
+ // src/core/agent/sessions/index.ts
41497
+ import {
41498
+ mkdirSync,
41499
+ existsSync,
41500
+ writeFileSync,
41491
41501
  readFileSync,
41492
- existsSync
41502
+ readdirSync,
41503
+ statSync,
41504
+ rmSync
41493
41505
  } from "fs";
41494
41506
  import { join } from "path";
41507
+ import { homedir } from "os";
41508
+
41509
+ // src/core/services/rateLimiter/index.ts
41510
+ function sleep(ms) {
41511
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
41512
+ }
41513
+
41514
+ class RateLimiter {
41515
+ tokens;
41516
+ lastRefillTime;
41517
+ rps;
41518
+ bucketSize;
41519
+ msPerToken;
41520
+ queue;
41521
+ constructor(config2) {
41522
+ this.rps = config2?.requestsPerSecond;
41523
+ this.bucketSize = this.rps ? 1 : 0;
41524
+ this.tokens = this.bucketSize;
41525
+ this.lastRefillTime = performance.now();
41526
+ this.msPerToken = this.rps ? 1000 / this.rps : undefined;
41527
+ this.queue = Promise.resolve();
41528
+ }
41529
+ async acquireSlot() {
41530
+ if (!this.rps || !this.msPerToken)
41531
+ return;
41532
+ const previousPromise = this.queue;
41533
+ let resolveCurrentRequest;
41534
+ this.queue = new Promise((resolve2) => {
41535
+ resolveCurrentRequest = resolve2;
41536
+ });
41537
+ await previousPromise;
41538
+ try {
41539
+ const now2 = performance.now();
41540
+ this.refill(now2);
41541
+ if (this.tokens < 1) {
41542
+ const waitTime = (1 - this.tokens) * this.msPerToken;
41543
+ await sleep(waitTime);
41544
+ const nowAfterSleep = performance.now();
41545
+ this.refill(nowAfterSleep);
41546
+ }
41547
+ this.tokens -= 1;
41548
+ } finally {
41549
+ resolveCurrentRequest();
41550
+ }
41551
+ }
41552
+ refill(now2) {
41553
+ if (this.tokens >= this.bucketSize) {
41554
+ this.lastRefillTime = now2;
41555
+ return;
41556
+ }
41557
+ const elapsed = now2 - this.lastRefillTime;
41558
+ const tokensToAdd = elapsed / this.msPerToken;
41559
+ this.tokens = Math.min(this.bucketSize, this.tokens + tokensToAdd);
41560
+ this.lastRefillTime = now2;
41561
+ }
41562
+ isEnabled() {
41563
+ return this.rps !== undefined;
41564
+ }
41565
+ }
41566
+
41567
+ // src/core/agent/sessions/index.ts
41568
+ var DEFAULT_OFFENSIVE_HEADERS = {
41569
+ "User-Agent": "pensar-apex"
41570
+ };
41571
+ function generateSessionId(prefix) {
41572
+ const timestamp = Date.now().toString(36);
41573
+ return `${prefix ? `${prefix}-` : ""}${timestamp}`;
41574
+ }
41575
+ function getPensarDir() {
41576
+ return join(homedir(), ".pensar");
41577
+ }
41578
+ function getExecutionsDir() {
41579
+ return join(getPensarDir(), "executions");
41580
+ }
41581
+ function createSession(target, objective, prefix, config2) {
41582
+ const sessionId = generateSessionId(prefix);
41583
+ const rootPath = join(getExecutionsDir(), sessionId);
41584
+ const findingsPath = join(rootPath, "findings");
41585
+ const scratchpadPath = join(rootPath, "scratchpad");
41586
+ const logsPath = join(rootPath, "logs");
41587
+ ensureDirectoryExists(rootPath);
41588
+ ensureDirectoryExists(findingsPath);
41589
+ ensureDirectoryExists(scratchpadPath);
41590
+ ensureDirectoryExists(logsPath);
41591
+ const session = {
41592
+ id: sessionId,
41593
+ rootPath,
41594
+ findingsPath,
41595
+ scratchpadPath,
41596
+ logsPath,
41597
+ target,
41598
+ objective: objective ?? "",
41599
+ startTime: new Date().toISOString(),
41600
+ config: config2
41601
+ };
41602
+ if (config2?.rateLimiter) {
41603
+ session._rateLimiter = new RateLimiter(config2.rateLimiter);
41604
+ }
41605
+ const metadataPath = join(rootPath, "session.json");
41606
+ writeFileSync(metadataPath, JSON.stringify(session, null, 2));
41607
+ const readmePath = join(rootPath, "README.md");
41608
+ const readme = `# Penetration Test Session
41609
+
41610
+ **Session ID:** ${sessionId}
41611
+ **Target:** ${target}
41612
+ **Objective:** ${objective}
41613
+ **Started:** ${session.startTime}
41614
+
41615
+ ## Directory Structure
41616
+
41617
+ - \`findings/\` - Security findings and vulnerabilities
41618
+ - \`scratchpad/\` - Notes and temporary data during testing
41619
+ - \`logs/\` - Execution logs and command outputs
41620
+ - \`session.json\` - Session metadata
41621
+
41622
+ ## Findings
41623
+
41624
+ Security findings will be documented in the \`findings/\` directory as individual files.
41625
+
41626
+ ## Status
41627
+
41628
+ Testing in progress...
41629
+ `;
41630
+ writeFileSync(readmePath, readme);
41631
+ return session;
41632
+ }
41633
+ function ensureDirectoryExists(path) {
41634
+ if (!existsSync(path)) {
41635
+ mkdirSync(path, { recursive: true });
41636
+ }
41637
+ }
41638
+ function getOffensiveHeaders(session) {
41639
+ const config2 = session.config?.offensiveHeaders;
41640
+ if (!config2 || config2.mode === "none") {
41641
+ return;
41642
+ }
41643
+ if (config2.mode === "default") {
41644
+ return DEFAULT_OFFENSIVE_HEADERS;
41645
+ }
41646
+ if (config2.mode === "custom" && config2.headers) {
41647
+ return config2.headers;
41648
+ }
41649
+ return;
41650
+ }
41651
+ // src/core/agent/tools.ts
41495
41652
  var execAsync = promisify(exec);
41496
41653
  var PayloadSchema = exports_external.object({
41497
41654
  payload: exports_external.string(),
@@ -42585,7 +42742,7 @@ FINDING STRUCTURE:
42585
42742
  const safeTitle = finding.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 50);
42586
42743
  const findingId = `${timestamp.split("T")[0]}-${safeTitle}`;
42587
42744
  const filename = `${findingId}.md`;
42588
- const filepath = join(session.findingsPath, filename);
42745
+ const filepath = join2(session.findingsPath, filename);
42589
42746
  const markdown = `# ${finding.title}
42590
42747
 
42591
42748
  **Severity:** ${finding.severity}
@@ -42619,8 +42776,8 @@ ${finding.references}` : ""}
42619
42776
 
42620
42777
  *This finding was automatically documented by the Pensar penetration testing agent.*
42621
42778
  `;
42622
- writeFileSync(filepath, markdown);
42623
- const summaryPath = join(session.rootPath, "findings-summary.md");
42779
+ writeFileSync2(filepath, markdown);
42780
+ const summaryPath = join2(session.rootPath, "findings-summary.md");
42624
42781
  const summaryEntry = `- [${finding.severity}] ${finding.title} - \`findings/${filename}\`
42625
42782
  `;
42626
42783
  try {
@@ -42634,7 +42791,7 @@ ${finding.references}` : ""}
42634
42791
  ## All Findings
42635
42792
 
42636
42793
  `;
42637
- writeFileSync(summaryPath, header + summaryEntry);
42794
+ writeFileSync2(summaryPath, header + summaryEntry);
42638
42795
  }
42639
42796
  return {
42640
42797
  success: true,
@@ -42673,7 +42830,7 @@ The scratchpad is session-specific and helps maintain context during long assess
42673
42830
  execute: async ({ note, category }) => {
42674
42831
  try {
42675
42832
  const timestamp = new Date().toISOString();
42676
- const scratchpadFile = join(session.scratchpadPath, "notes.md");
42833
+ const scratchpadFile = join2(session.scratchpadPath, "notes.md");
42677
42834
  const entry = `## ${category.toUpperCase()} - ${timestamp}
42678
42835
 
42679
42836
  ${note}
@@ -42692,7 +42849,7 @@ ${note}
42692
42849
  ---
42693
42850
 
42694
42851
  `;
42695
- writeFileSync(scratchpadFile, header + entry);
42852
+ writeFileSync2(scratchpadFile, header + entry);
42696
42853
  }
42697
42854
  return {
42698
42855
  success: true,
@@ -42819,11 +42976,11 @@ The report will be saved as 'pentest-report.md' in the session root directory.`,
42819
42976
  MEDIUM: 0,
42820
42977
  LOW: 0
42821
42978
  };
42822
- if (existsSync(session.findingsPath)) {
42823
- const findingFiles = readdirSync(session.findingsPath).filter((f) => f.endsWith(".json"));
42979
+ if (existsSync2(session.findingsPath)) {
42980
+ const findingFiles = readdirSync2(session.findingsPath).filter((f) => f.endsWith(".json"));
42824
42981
  for (const file2 of findingFiles) {
42825
- const filePath = join(session.findingsPath, file2);
42826
- const content = readFileSync(filePath, "utf-8");
42982
+ const filePath = join2(session.findingsPath, file2);
42983
+ const content = readFileSync2(filePath, "utf-8");
42827
42984
  const severityMatch = content.match(/\*\*Severity:\*\*\s+(CRITICAL|HIGH|MEDIUM|LOW)/);
42828
42985
  const titleMatch = content.match(/^#\s+(.+)$/m);
42829
42986
  if (severityMatch && titleMatch) {
@@ -42848,14 +43005,14 @@ The report will be saved as 'pentest-report.md' in the session root directory.`,
42848
43005
  const totalFindings = findings.length;
42849
43006
  const criticalAndHigh = severityCounts.CRITICAL + severityCounts.HIGH;
42850
43007
  let scratchpadNotes = "";
42851
- const scratchpadFile = join(session.scratchpadPath, "notes.md");
42852
- if (existsSync(scratchpadFile)) {
42853
- scratchpadNotes = readFileSync(scratchpadFile, "utf-8");
43008
+ const scratchpadFile = join2(session.scratchpadPath, "notes.md");
43009
+ if (existsSync2(scratchpadFile)) {
43010
+ scratchpadNotes = readFileSync2(scratchpadFile, "utf-8");
42854
43011
  }
42855
43012
  let testResultsSummary = "";
42856
- const testResultsFile = join(session.scratchpadPath, "test-results.jsonl");
42857
- if (existsSync(testResultsFile)) {
42858
- const testLines = readFileSync(testResultsFile, "utf-8").split(`
43013
+ const testResultsFile = join2(session.scratchpadPath, "test-results.jsonl");
43014
+ if (existsSync2(testResultsFile)) {
43015
+ const testLines = readFileSync2(testResultsFile, "utf-8").split(`
42859
43016
  `).filter((l) => l.trim());
42860
43017
  const testResults = testLines.map((line) => {
42861
43018
  try {
@@ -43029,25 +43186,25 @@ This report should be treated as confidential and distributed only to authorized
43029
43186
  *Report generated by Pensar Penetration Testing Agent*
43030
43187
  *Session: ${session.id}*
43031
43188
  `;
43032
- const reportPath = join(session.rootPath, "pentest-report.md");
43033
- writeFileSync(reportPath, report);
43034
- const readmePath = join(session.rootPath, "README.md");
43035
- if (existsSync(readmePath)) {
43036
- let readme = readFileSync(readmePath, "utf-8");
43189
+ const reportPath = join2(session.rootPath, "pentest-report.md");
43190
+ writeFileSync2(reportPath, report);
43191
+ const readmePath = join2(session.rootPath, "README.md");
43192
+ if (existsSync2(readmePath)) {
43193
+ let readme = readFileSync2(readmePath, "utf-8");
43037
43194
  readme = readme.replace("Testing in progress...", `Testing completed on ${endDate.toLocaleString()}
43038
43195
 
43039
43196
  **Final Report:** \`pentest-report.md\``);
43040
- writeFileSync(readmePath, readme);
43197
+ writeFileSync2(readmePath, readme);
43041
43198
  }
43042
- const metadataPath = join(session.rootPath, "session.json");
43043
- if (existsSync(metadataPath)) {
43044
- const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
43199
+ const metadataPath = join2(session.rootPath, "session.json");
43200
+ if (existsSync2(metadataPath)) {
43201
+ const metadata = JSON.parse(readFileSync2(metadataPath, "utf-8"));
43045
43202
  metadata.endTime = endTime;
43046
43203
  metadata.duration = duration3;
43047
43204
  metadata.status = "completed";
43048
43205
  metadata.totalFindings = totalFindings;
43049
43206
  metadata.severityCounts = severityCounts;
43050
- writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
43207
+ writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2));
43051
43208
  }
43052
43209
  return {
43053
43210
  success: true,
@@ -43088,7 +43245,7 @@ async function recordTestResultCore(session, params) {
43088
43245
  evidence: params.evidence || "",
43089
43246
  confidence: params.confidence || "high"
43090
43247
  };
43091
- const testResultsPath = join(session.scratchpadPath, "test-results.jsonl");
43248
+ const testResultsPath = join2(session.scratchpadPath, "test-results.jsonl");
43092
43249
  const resultLine = JSON.stringify(testResult) + `
43093
43250
  `;
43094
43251
  appendFileSync(testResultsPath, resultLine);
@@ -43499,8 +43656,8 @@ Use this when:
43499
43656
  }),
43500
43657
  execute: async ({ objective }) => {
43501
43658
  try {
43502
- const testResultsPath = join(session.scratchpadPath, "test-results.jsonl");
43503
- if (!existsSync(testResultsPath)) {
43659
+ const testResultsPath = join2(session.scratchpadPath, "test-results.jsonl");
43660
+ if (!existsSync2(testResultsPath)) {
43504
43661
  return {
43505
43662
  success: true,
43506
43663
  totalTests: 0,
@@ -43509,7 +43666,7 @@ Use this when:
43509
43666
  suggestions: objective ? `Based on objective "${objective}", consider testing relevant parameters with test_parameter tool.` : "Use test_parameter to test parameters for vulnerabilities."
43510
43667
  };
43511
43668
  }
43512
- const fileContent = readFileSync(testResultsPath, "utf-8");
43669
+ const fileContent = readFileSync2(testResultsPath, "utf-8");
43513
43670
  const testResults = fileContent.trim().split(`
43514
43671
  `).filter((line) => line.trim()).map((line) => JSON.parse(line));
43515
43672
  const parametersTested = new Set;
@@ -43766,7 +43923,7 @@ ${discovered.map((d) => `- ${d.endpoint} [${d.method}] → HTTP ${d.status}`).jo
43766
43923
  Not found: ${range.max - range.min + 1 - discovered.length} endpoints returned 404`;
43767
43924
  try {
43768
43925
  const timestamp = new Date().toISOString();
43769
- const scratchpadFile = join(session.scratchpadPath, "notes.md");
43926
+ const scratchpadFile = join2(session.scratchpadPath, "notes.md");
43770
43927
  const entry = `## RESULT - ${timestamp}
43771
43928
 
43772
43929
  ${note}
@@ -43785,7 +43942,7 @@ ${note}
43785
43942
  ---
43786
43943
 
43787
43944
  `;
43788
- writeFileSync(scratchpadFile, header + entry);
43945
+ writeFileSync2(scratchpadFile, header + entry);
43789
43946
  }
43790
43947
  } catch (err) {
43791
43948
  console.error("Failed to record to scratchpad:", err);
@@ -43802,7 +43959,53 @@ ${note}
43802
43959
  }
43803
43960
  });
43804
43961
  }
43962
+ function wrapCommandWithHeaders(command, headers) {
43963
+ const userAgent = headers["User-Agent"];
43964
+ if (!userAgent)
43965
+ return command;
43966
+ let wrapped = command;
43967
+ if (command.includes("curl") && !command.includes("User-Agent") && !command.includes("-A")) {
43968
+ wrapped = wrapped.replace(/\bcurl(\s+)/g, `curl -A "${userAgent}" $1`);
43969
+ }
43970
+ if (command.includes("nikto") && !command.includes("-useragent")) {
43971
+ wrapped = wrapped.replace(/\bnikto\s+/, `nikto -useragent "${userAgent}" `);
43972
+ }
43973
+ if (command.includes("nmap") && command.includes("--script") && /--script[= ](.*?http.*?)/.test(command) && !command.includes("http.useragent")) {
43974
+ if (command.includes("--script-args")) {
43975
+ wrapped = wrapped.replace(/--script-args\s+([^\s]+)/, `--script-args $1,http.useragent="${userAgent}"`);
43976
+ } else {
43977
+ wrapped = `${wrapped} --script-args http.useragent="${userAgent}"`;
43978
+ }
43979
+ }
43980
+ if (command.includes("gobuster") && !command.includes("-a ") && !command.includes("--useragent")) {
43981
+ wrapped = wrapped.replace(/\bgobuster\s+/, `gobuster -a "${userAgent}" `);
43982
+ }
43983
+ if (command.includes("ffuf") && !command.includes("User-Agent:")) {
43984
+ wrapped = wrapped.replace(/\bffuf\s+/, `ffuf -H "User-Agent: ${userAgent}" `);
43985
+ }
43986
+ if (command.includes("sqlmap") && !command.includes("--user-agent")) {
43987
+ wrapped = wrapped.replace(/\bsqlmap\s+/, `sqlmap --user-agent="${userAgent}" `);
43988
+ }
43989
+ if (command.includes("wfuzz") && !command.includes("-H") && !command.includes("User-Agent")) {
43990
+ wrapped = wrapped.replace(/\bwfuzz\s+/, `wfuzz -H "User-Agent: ${userAgent}" `);
43991
+ }
43992
+ if (command.includes("dirb") && !command.includes("-a")) {
43993
+ wrapped = wrapped.replace(/\bdirb\s+/, `dirb -a "${userAgent}" `);
43994
+ }
43995
+ if (command.includes("wpscan") && !command.includes("--user-agent")) {
43996
+ wrapped = wrapped.replace(/\bwpscan\s+/, `wpscan --user-agent "${userAgent}" `);
43997
+ }
43998
+ if (command.includes("nuclei") && !command.includes("-H")) {
43999
+ wrapped = wrapped.replace(/\bnuclei\s+/, `nuclei -H "User-Agent: ${userAgent}" `);
44000
+ }
44001
+ if (command.includes("httpx") && !command.includes("-H")) {
44002
+ wrapped = wrapped.replace(/\bhttpx\s+/, `httpx -H "User-Agent: ${userAgent}" `);
44003
+ }
44004
+ return wrapped;
44005
+ }
43805
44006
  function createPentestTools(session, model, toolOverride) {
44007
+ const offensiveHeaders = getOffensiveHeaders(session);
44008
+ const rateLimiter = session._rateLimiter;
43806
44009
  const executeCommand = tool({
43807
44010
  name: "execute_command",
43808
44011
  description: `Execute a shell command for penetration testing activities.
@@ -43847,6 +44050,9 @@ IMPORTANT: Always analyze results and adjust your approach based on findings.`,
43847
44050
  inputSchema: ExecuteCommandInput,
43848
44051
  execute: async ({ command, timeout = 30000, toolCallDescription }) => {
43849
44052
  try {
44053
+ if (rateLimiter) {
44054
+ await rateLimiter.acquireSlot();
44055
+ }
43850
44056
  if (toolOverride?.execute_command) {
43851
44057
  return toolOverride.execute_command({
43852
44058
  command,
@@ -43854,7 +44060,8 @@ IMPORTANT: Always analyze results and adjust your approach based on findings.`,
43854
44060
  toolCallDescription
43855
44061
  });
43856
44062
  }
43857
- const { stdout, stderr } = await execAsync(command, {
44063
+ const finalCommand = offensiveHeaders ? wrapCommandWithHeaders(command, offensiveHeaders) : command;
44064
+ const { stdout, stderr } = await execAsync(finalCommand, {
43858
44065
  timeout,
43859
44066
  maxBuffer: 10 * 1024 * 1024
43860
44067
  });
@@ -43864,7 +44071,7 @@ IMPORTANT: Always analyze results and adjust your approach based on findings.`,
43864
44071
 
43865
44072
  (truncated) call the command again with grep / tail to paginate the response` || "(no output)",
43866
44073
  stderr: stderr || "",
43867
- command,
44074
+ command: finalCommand,
43868
44075
  error: ""
43869
44076
  };
43870
44077
  } catch (error46) {
@@ -43902,6 +44109,9 @@ COMMON TESTING PATTERNS:
43902
44109
  inputSchema: HttpRequestInput,
43903
44110
  execute: async ({ url: url2, method, headers, body, followRedirects, timeout, toolCallDescription }) => {
43904
44111
  try {
44112
+ if (rateLimiter) {
44113
+ await rateLimiter.acquireSlot();
44114
+ }
43905
44115
  if (toolOverride?.http_request) {
43906
44116
  return toolOverride.http_request({
43907
44117
  url: url2,
@@ -43917,7 +44127,10 @@ COMMON TESTING PATTERNS:
43917
44127
  const timeoutId = setTimeout(() => controller.abort(), timeout);
43918
44128
  const response = await fetch(url2, {
43919
44129
  method,
43920
- headers: headers || {},
44130
+ headers: {
44131
+ ...offensiveHeaders || {},
44132
+ ...headers || {}
44133
+ },
43921
44134
  body: body || undefined,
43922
44135
  redirect: followRedirects ? "follow" : "manual",
43923
44136
  signal: controller.signal
@@ -43976,82 +44189,6 @@ COMMON TESTING PATTERNS:
43976
44189
  };
43977
44190
  }
43978
44191
 
43979
- // src/core/agent/sessions/index.ts
43980
- import {
43981
- mkdirSync,
43982
- existsSync as existsSync2,
43983
- writeFileSync as writeFileSync2,
43984
- readFileSync as readFileSync2,
43985
- readdirSync as readdirSync2,
43986
- statSync,
43987
- rmSync
43988
- } from "fs";
43989
- import { join as join2 } from "path";
43990
- import { homedir } from "os";
43991
- function generateSessionId(prefix) {
43992
- const timestamp = Date.now().toString(36);
43993
- return `${prefix ? `${prefix}-` : ""}${timestamp}`;
43994
- }
43995
- function getPensarDir() {
43996
- return join2(homedir(), ".pensar");
43997
- }
43998
- function getExecutionsDir() {
43999
- return join2(getPensarDir(), "executions");
44000
- }
44001
- function createSession(target, objective, prefix) {
44002
- const sessionId = generateSessionId(prefix);
44003
- const rootPath = join2(getExecutionsDir(), sessionId);
44004
- const findingsPath = join2(rootPath, "findings");
44005
- const scratchpadPath = join2(rootPath, "scratchpad");
44006
- const logsPath = join2(rootPath, "logs");
44007
- ensureDirectoryExists(rootPath);
44008
- ensureDirectoryExists(findingsPath);
44009
- ensureDirectoryExists(scratchpadPath);
44010
- ensureDirectoryExists(logsPath);
44011
- const session = {
44012
- id: sessionId,
44013
- rootPath,
44014
- findingsPath,
44015
- scratchpadPath,
44016
- logsPath,
44017
- target,
44018
- objective: objective ?? "",
44019
- startTime: new Date().toISOString()
44020
- };
44021
- const metadataPath = join2(rootPath, "session.json");
44022
- writeFileSync2(metadataPath, JSON.stringify(session, null, 2));
44023
- const readmePath = join2(rootPath, "README.md");
44024
- const readme = `# Penetration Test Session
44025
-
44026
- **Session ID:** ${sessionId}
44027
- **Target:** ${target}
44028
- **Objective:** ${objective}
44029
- **Started:** ${session.startTime}
44030
-
44031
- ## Directory Structure
44032
-
44033
- - \`findings/\` - Security findings and vulnerabilities
44034
- - \`scratchpad/\` - Notes and temporary data during testing
44035
- - \`logs/\` - Execution logs and command outputs
44036
- - \`session.json\` - Session metadata
44037
-
44038
- ## Findings
44039
-
44040
- Security findings will be documented in the \`findings/\` directory as individual files.
44041
-
44042
- ## Status
44043
-
44044
- Testing in progress...
44045
- `;
44046
- writeFileSync2(readmePath, readme);
44047
- return session;
44048
- }
44049
- function ensureDirectoryExists(path) {
44050
- if (!existsSync2(path)) {
44051
- mkdirSync(path, { recursive: true });
44052
- }
44053
- }
44054
-
44055
44192
  // src/core/agent/utils.ts
44056
44193
  import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
44057
44194
  import { execSync } from "child_process";
@@ -44866,9 +45003,10 @@ function runAgent(opts) {
44866
45003
  silent,
44867
45004
  authConfig,
44868
45005
  toolOverride,
44869
- messages
45006
+ messages,
45007
+ sessionConfig
44870
45008
  } = opts;
44871
- const session = opts.session || createSession(target, objective);
45009
+ const session = opts.session || createSession(target, objective, undefined, sessionConfig);
44872
45010
  const pocsPath = join4(session.rootPath, "pocs");
44873
45011
  if (!existsSync6(pocsPath)) {
44874
45012
  mkdirSync4(pocsPath, { recursive: true });
@@ -45032,19 +45170,45 @@ You are only authorized to perform testing against the specific target endpoint
45032
45170
 
45033
45171
  // scripts/quicktest.ts
45034
45172
  async function runQuicktest(options) {
45035
- const { target, objective, model = "claude-sonnet-4-5" } = options;
45173
+ const {
45174
+ target,
45175
+ objective,
45176
+ model = "claude-sonnet-4-5",
45177
+ headerMode = "default",
45178
+ customHeaders,
45179
+ rps
45180
+ } = options;
45036
45181
  console.log("=".repeat(80));
45037
45182
  console.log("PENSAR QUICK PENTEST");
45038
45183
  console.log("=".repeat(80));
45039
45184
  console.log(`Target: ${target}`);
45040
45185
  console.log(`Objective: ${objective}`);
45041
45186
  console.log(`Model: ${model}`);
45187
+ console.log(`Rate Limit: ${rps ? `${rps} req/s` : "Unlimited"}`);
45188
+ console.log(`Headers: ${headerMode === "none" ? "None" : headerMode === "default" ? "Default (pensar-apex)" : "Custom"}`);
45189
+ if (headerMode === "custom" && customHeaders) {
45190
+ for (const [key, value] of Object.entries(customHeaders)) {
45191
+ console.log(` ${key}: ${value}`);
45192
+ }
45193
+ }
45042
45194
  console.log();
45043
45195
  try {
45196
+ const sessionConfig = {
45197
+ offensiveHeaders: {
45198
+ mode: headerMode,
45199
+ headers: headerMode === "custom" ? customHeaders : undefined
45200
+ },
45201
+ ...rps && {
45202
+ rateLimiter: {
45203
+ requestsPerSecond: rps
45204
+ }
45205
+ }
45206
+ };
45044
45207
  const { streamResult, session } = runAgent({
45045
45208
  target,
45046
45209
  objective,
45047
- model
45210
+ model,
45211
+ sessionConfig
45048
45212
  });
45049
45213
  console.log(`Session ID: ${session.id}`);
45050
45214
  console.log(`Session Path: ${session.rootPath}`);
@@ -45085,17 +45249,28 @@ async function runQuicktest(options) {
45085
45249
  async function main() {
45086
45250
  const args = process.argv.slice(2);
45087
45251
  if (args.length === 0) {
45088
- console.error("Usage: pensar quicktest --target <target> --objective <objective> [--model <model>]");
45252
+ console.error("Usage: pensar quicktest --target <target> --objective <objective> [options]");
45253
+ console.error();
45254
+ console.error("Required:");
45255
+ console.error(" --target <target> Target URL or IP address to test");
45256
+ console.error(" --objective <objective> Objective or goal of the pentest");
45089
45257
  console.error();
45090
45258
  console.error("Options:");
45091
- console.error(" --target <target> Target URL or IP address to test (required)");
45092
- console.error(" --objective <objective> Objective or goal of the pentest (required)");
45093
45259
  console.error(" --model <model> AI model to use (default: claude-sonnet-4-5)");
45260
+ console.error(" --rps <number> Rate limit: requests per second (default: unlimited)");
45261
+ console.error(" --headers <mode> Header mode: none, default, or custom (default: default)");
45262
+ console.error(" --header <name:value> Add custom header (requires --headers custom, can be repeated)");
45263
+ console.error();
45264
+ console.error("Header Modes:");
45265
+ console.error(" none No custom headers added to requests");
45266
+ console.error(" default Add 'User-Agent: pensar-apex' to all offensive requests");
45267
+ console.error(" custom Use custom headers defined with --header flag");
45094
45268
  console.error();
45095
45269
  console.error("Examples:");
45096
- console.error(" pensar quicktest --target http://localhost:3000 --objective 'Find SQL injection vulnerabilities'");
45097
- console.error(" pensar quicktest --target 192.168.1.100 --objective 'Test for authentication bypass' --model gpt-4o");
45098
- console.error(" pensar quicktest --target https://example.com --objective 'Find XSS vulnerabilities' --model claude-opus-4-1");
45270
+ console.error(" pensar quicktest --target http://localhost:3000 --objective 'Find SQL injection'");
45271
+ console.error(" pensar quicktest --target 192.168.1.100 --objective 'Test auth bypass' --headers none");
45272
+ console.error(" pensar quicktest --target api.example.com --objective 'API testing' --rps 5 \\");
45273
+ console.error(" --headers custom --header 'User-Agent: pensar_client123' --header 'X-Bug-Bounty: researcher'");
45099
45274
  console.error();
45100
45275
  process.exit(1);
45101
45276
  }
@@ -45129,11 +45304,69 @@ async function main() {
45129
45304
  }
45130
45305
  model = modelArg;
45131
45306
  }
45307
+ const rpsIndex = args.indexOf("--rps");
45308
+ let rps;
45309
+ if (rpsIndex !== -1) {
45310
+ const rpsArg = args[rpsIndex + 1];
45311
+ if (!rpsArg) {
45312
+ console.error("Error: --rps must be followed by a number");
45313
+ process.exit(1);
45314
+ }
45315
+ const parsedRps = parseInt(rpsArg, 10);
45316
+ if (isNaN(parsedRps) || parsedRps <= 0) {
45317
+ console.error("Error: --rps must be a positive number");
45318
+ process.exit(1);
45319
+ }
45320
+ rps = parsedRps;
45321
+ }
45322
+ const headersIndex = args.indexOf("--headers");
45323
+ let headerMode = "default";
45324
+ if (headersIndex !== -1) {
45325
+ const headersArg = args[headersIndex + 1];
45326
+ if (!headersArg || !["none", "default", "custom"].includes(headersArg)) {
45327
+ console.error("Error: --headers must be 'none', 'default', or 'custom'");
45328
+ process.exit(1);
45329
+ }
45330
+ headerMode = headersArg;
45331
+ }
45332
+ const customHeaders = {};
45333
+ for (let i = 0;i < args.length; i++) {
45334
+ if (args[i] === "--header") {
45335
+ const headerArg = args[i + 1];
45336
+ if (!headerArg) {
45337
+ console.error("Error: --header must be followed by 'Name: Value'");
45338
+ process.exit(1);
45339
+ }
45340
+ const colonIndex = headerArg.indexOf(":");
45341
+ if (colonIndex === -1) {
45342
+ console.error("Error: --header must be in format 'Name: Value'");
45343
+ process.exit(1);
45344
+ }
45345
+ const name18 = headerArg.substring(0, colonIndex).trim();
45346
+ const value = headerArg.substring(colonIndex + 1).trim();
45347
+ if (!name18) {
45348
+ console.error("Error: Header name cannot be empty");
45349
+ process.exit(1);
45350
+ }
45351
+ customHeaders[name18] = value;
45352
+ }
45353
+ }
45354
+ if (headerMode !== "custom" && Object.keys(customHeaders).length > 0) {
45355
+ console.error("Error: --header flag requires --headers custom");
45356
+ process.exit(1);
45357
+ }
45358
+ if (headerMode === "custom" && Object.keys(customHeaders).length === 0) {
45359
+ console.error("Error: --headers custom requires at least one --header flag");
45360
+ process.exit(1);
45361
+ }
45132
45362
  try {
45133
45363
  await runQuicktest({
45134
45364
  target,
45135
45365
  objective,
45136
- ...model && { model }
45366
+ ...model && { model },
45367
+ headerMode,
45368
+ ...headerMode === "custom" && { customHeaders },
45369
+ ...rps && { rps }
45137
45370
  });
45138
45371
  } catch (error46) {
45139
45372
  console.error("Fatal error:", error46.message);