@agentsoc/beacon 0.0.6 → 0.0.7
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.
- package/README.md +4 -2
- package/dist/cli.js +242 -97
- package/dist/service-install.d.ts.map +1 -1
- package/dist/service-install.js +10 -4
- package/dist/tail.d.ts +3 -0
- package/dist/tail.d.ts.map +1 -1
- package/dist/tail.js +76 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,10 +95,12 @@ _Note: You can use `AGENTSOC_API_KEY` instead of saving a key in config. Use `AG
|
|
|
95
95
|
Install the background service daemon (systemd on Linux, launchd on macOS):
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
|
-
sudo beacon install
|
|
98
|
+
sudo "$(command -v beacon)" install
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
On macOS the plist is written under `/Library/LaunchDaemons/`, and on Linux the unit file goes under `/etc/systemd/system/`—both require root
|
|
101
|
+
On macOS the plist is written under `/Library/LaunchDaemons/`, and on Linux the unit file goes under `/etc/systemd/system/`—both require root. The service **runs as your login user** (via `SUDO_USER` when you use `sudo`), so it uses the same config directory as `beacon config`—not root’s home.
|
|
102
|
+
|
|
103
|
+
**`sudo beacon` says “command not found”:** sudo resets `PATH`, so it often cannot see npm/nvm global bins. Use `sudo "$(command -v beacon)" install` (as above), or `sudo env "PATH=$PATH" beacon install`, or the full path from `which beacon`.
|
|
102
104
|
|
|
103
105
|
### Status and stats
|
|
104
106
|
|
package/dist/cli.js
CHANGED
|
@@ -1,98 +1,243 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`)}),this}}function K0($,z){if($._hasHelpOption&&z.find((Q)=>Q===$._helpLongFlag||Q===$._helpShortFlag))$.outputHelp(),$._exit(0,"commander.helpDisplayed","(outputHelp)")}function L0($){return $.map((z)=>{if(!z.startsWith("--inspect"))return z;let J,Q="127.0.0.1",Y="9229",Z;if((Z=z.match(/^(--inspect(-brk)?)$/))!==null)J=Z[1];else if((Z=z.match(/^(--inspect(-brk|-port)?)=([^:]+)$/))!==null)if(J=Z[1],/^\d+$/.test(Z[3]))Y=Z[3];else Q=Z[3];else if((Z=z.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/))!==null)J=Z[1],Q=Z[3],Y=Z[4];if(J&&Y!=="0")return`${J}=${Q}:${parseInt(Y)+1}`;return z})}D1.Command=m});var j0=j((L,T0)=>{var{Argument:E1}=F(),{Command:H0}=R0(),{CommanderError:F1,InvalidArgumentError:_0}=N(),{Help:w1}=h(),{Option:k1}=u();L=T0.exports=new H0;L.program=L;L.Command=H0;L.Option=k1;L.Argument=E1;L.Help=w1;L.CommanderError=F1;L.InvalidArgumentError=_0;L.InvalidOptionArgumentError=_0});import{execFileSync as X$}from"node:child_process";var I0=o0(j0(),1),{program:_$,createCommand:T$,createArgument:j$,createOption:I$,CommanderError:S$,InvalidArgumentError:V$,InvalidOptionArgumentError:N$,Command:S0,Argument:D$,Option:P$,Help:E$}=I0.default;import y1 from"node:os";import S from"node:path";import d from"node:fs/promises";var A1="https://ingest.agentsoc.com/api/v1/webhooks/syslog",C1="http://localhost:8110/api/v1/webhooks/syslog",b1="https://api.agentsoc.com",f1="http://localhost:8100",O1="/api/v1/siem/beacon/validate/key";function s($){let z=process.env.AGENTSOC_API_KEY?.trim();if(z)return z;return $.apiKey?.trim()||void 0}var D="Beacon is not configured. Run: beacon config --key <your-api-key> [--env production|development]. Or set AGENTSOC_API_KEY in your environment.";function w($){let z=$.toLowerCase().trim();if(z==="production"||z==="prod")return"production";if(z==="development"||z==="dev")return"development";throw Error(`Invalid environment "${$}". Use production or development (prod / dev).`)}function i($){if($==="production"||$==="development")return $;if(typeof $==="string")try{return w($)}catch{return}return}function x1($){return $==="development"?C1:A1}function v1($){return $==="development"?f1:b1}function h1(){let $=process.env.AGENTSOC_ENV?.trim();if(!$)return;try{return w($)}catch{console.warn(`[beacon] Ignoring invalid AGENTSOC_ENV="${$}" (use production or development).`);return}}function V0($){return h1()??i($.environment)??"production"}function N0($){let z=process.env.AGENTSOC_INGEST_URL?.trim();if(z)return z;return x1(V0($))}function u1($){let z=process.env.AGENTSOC_PLATFORM_URL?.trim()||process.env.AGENTSOC_PLATFORM_API_BASE_URL?.trim();if(z)return z.replace(/\/$/,"");return v1(V0($))}function D0($){return`${u1($)}${O1}`}function k(){let $=y1.homedir();if(process.platform==="win32")return S.join(process.env.APPDATA||S.join($,"AppData","Roaming"),"agentsoc-beacon");if(process.platform==="darwin")return S.join($,"Library","Application Support","agentsoc-beacon");let z=process.env.XDG_CONFIG_HOME||S.join($,".config");return S.join(z,"agentsoc-beacon")}function p(){return S.join(k(),"config.json")}async function _(){try{let $=p(),z=await d.readFile($,"utf8");return JSON.parse(z)}catch($){return{}}}async function y($){let z=k();try{await d.mkdir(z,{recursive:!0}),await d.writeFile(p(),JSON.stringify($,null,2),"utf8")}catch(J){console.error(`[syslog-beacon] Failed to save config to ${p()}:`,J)}}async function P0($,z){let J=new AbortController,Q=setTimeout(()=>J.abort(),12000);try{let Y=await fetch($,{method:"GET",headers:{"X-API-Key":z},signal:J.signal}),Z=await Y.text(),B;try{B=Z?JSON.parse(Z):null}catch{return{ok:!1,message:"Invalid response from platform API"}}if(!Y.ok){let X=B;return{ok:!1,message:X&&typeof X.error==="string"?X.error:`HTTP ${Y.status}`}}let q=B;if(!q?.success||!q.apiKey?.name)return{ok:!1,message:"Unexpected response from platform API"};return{ok:!0,organizationName:q.organization?.name??null,organizationSlug:q.organization?.slug??null,apiKeyName:q.apiKey.name}}catch(Y){if(Y instanceof Error&&Y.name==="AbortError")return{ok:!1,message:"Request timed out"};return{ok:!1,message:Y instanceof Error?Y.message:String(Y)}}finally{clearTimeout(Q)}}var g1=`https://registry.npmjs.org/${encodeURIComponent("@agentsoc/beacon")}`;function c1($,z){return{success:!0,package:"@agentsoc/beacon",latestVersion:$,registryResolved:z,install:{npm:"npm install -g @agentsoc/beacon@latest",bun:"bun add -g @agentsoc/beacon@latest",pnpm:"pnpm add -g @agentsoc/beacon@latest"},docsUrl:"https://agentsoc.com/products/agentsoc-beacon",installScriptUrl:"https://agentsoc.com/connectors/beacon.sh"}}async function E0(){let $=new AbortController,z=setTimeout(()=>$.abort(),12000);try{let J=await fetch(g1,{headers:{Accept:"application/json"},signal:$.signal}),Q=await J.text(),Y;try{Y=Q?JSON.parse(Q):null}catch{return{ok:!1,message:"Invalid JSON from registry.npmjs.org"}}if(!J.ok)return{ok:!1,message:`registry.npmjs.org returned HTTP ${J.status}`};let B=Y["dist-tags"]?.latest?.trim()??null;return{ok:!0,info:c1(B,!0)}}catch(J){if(J instanceof Error&&J.name==="AbortError")return{ok:!1,message:"Request timed out"};return{ok:!1,message:J instanceof Error?J.message:String(J)}}finally{clearTimeout(z)}}import{existsSync as l1,readFileSync as m1}from"node:fs";import{dirname as F0,join as d1}from"node:path";import{fileURLToPath as p1}from"node:url";function k0(){let $=F0(p1(import.meta.url));for(let z=0;z<8;z++){let J=d1($,"package.json");if(l1(J))try{let Y=JSON.parse(m1(J,"utf8"));if(Y.name==="@agentsoc/beacon"&&typeof Y.version==="string")return Y.version}catch{}let Q=F0($);if(Q===$)break;$=Q}return"0.0.0"}function w0($){let z=$.indexOf("-");return z===-1?$:$.slice(0,z)}function n($,z){let J=w0($).split(".").map((Z)=>parseInt(Z,10)||0),Q=w0(z).split(".").map((Z)=>parseInt(Z,10)||0),Y=Math.max(J.length,Q.length);for(let Z=0;Z<Y;Z++){let B=J[Z]??0,q=Q[Z]??0;if(B<q)return!0;if(B>q)return!1}return!1}import o from"node:os";import s1 from"node:crypto";async function y0(){let $=await _(),z=$.agentId;if(!z)z=s1.randomUUID(),$.agentId=z,await y($);let J=o.hostname(),Q=o.platform(),Y="127.0.0.1",Z="00:00:00:00:00:00",B=o.networkInterfaces();for(let q of Object.keys(B)){let X=B[q];if(!X)continue;for(let G of X)if(!G.internal&&G.family==="IPv4"){Y=G.address,Z=G.mac;break}if(Y!=="127.0.0.1")break}return{agentId:z,hostname:J,ip:Y,osPlatform:Q,mac:Z}}import A from"node:fs/promises";import A0 from"node:path";var P=1,r=()=>({version:P,updatedAt:new Date(0).toISOString(),logsForwarded:0,batchesSucceeded:0,batchesFailed:0});function C(){return A0.join(k(),"stats.json")}async function b(){try{let $=await A.readFile(C(),"utf8"),z=JSON.parse($);if(z.version!==P)return r();return{...r(),...z,version:P}}catch{return r()}}async function C0($){let z=C(),J=A0.dirname(z);await A.mkdir(J,{recursive:!0});let Q=`${z}.${process.pid}.tmp`;await A.writeFile(Q,JSON.stringify($,null,2),"utf8"),await A.rename(Q,z)}async function b0($){try{let z=await b();await C0({version:P,updatedAt:new Date().toISOString(),logsForwarded:z.logsForwarded+$,batchesSucceeded:z.batchesSucceeded+1,batchesFailed:z.batchesFailed,lastError:void 0})}catch{}}async function t($){try{let z=await b(),J=$.replace(/\s+/g," ").slice(0,500);await C0({version:P,updatedAt:new Date().toISOString(),logsForwarded:z.logsForwarded,batchesSucceeded:z.batchesSucceeded,batchesFailed:z.batchesFailed+1,lastError:J||"Unknown error"})}catch{}}function i1($){let z=5,J=1,Q=$.match(/^<(\d+)>/);if(Q){let Z=parseInt(Q[1],10);J=Math.floor(Z/8),z=Z&7}else{let Z=$.toLowerCase();if(Z.includes("critical")||Z.includes("fatal")||Z.includes("panic"))z=2;else if(Z.includes("high")||Z.includes("error")||Z.includes("failed"))z=3;else if(Z.includes("medium")||Z.includes("warn"))z=4;else if(Z.includes("info"))z=6;else if(Z.includes("low")||Z.includes("notice"))z=5}let Y="info";if(z<=2)Y="critical";else if(z===3)Y="high";else if(z===4)Y="medium";else if(z===5)Y="low";return{severityStr:Y,facility:J}}function n1($){let z=$.match(/^\S+\s+(?:\S+\s+)?(\w+)(?:\[\d+\])?:\s*(.*)$/);if(z)return{app:z[1],message:z[2]};return{message:$}}class a{opts;buffer=[];timer=null;batchSize;flushMs;constructor($){this.opts=$;this.batchSize=$.batchSize||25,this.flushMs=$.flushMs||2000}pushLog($){let{severityStr:z,facility:J}=i1($),{app:Q,message:Y}=n1($),Z={full_log:Y,timestamp:new Date().toISOString(),hostname:this.opts.host.hostname,facility:J,severity:z,id:`${this.opts.host.agentId}-${Date.now()}-${Math.random().toString(36).slice(2,9)}`,agent:{id:this.opts.host.agentId,name:this.opts.host.hostname,ip:this.opts.host.ip,mac:this.opts.host.mac}};if(Q)Z.app=Q,Z.process={name:Q};if(this.buffer.push(Z),this.buffer.length>=this.batchSize)this.flush();else if(!this.timer)this.timer=setTimeout(()=>this.flush(),this.flushMs)}async flush(){if(this.timer)clearTimeout(this.timer),this.timer=null;if(this.buffer.length===0)return;let $=this.buffer;this.buffer=[],console.log(`[syslog-beacon] Pushing ${$.length} logs to API...`);try{let z=await fetch(this.opts.ingestUrl,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.opts.apiKey}`},body:JSON.stringify($)});if(!z.ok){let J=await z.text(),Q=`HTTP ${z.status} ${J}`;console.error(`[syslog-beacon] Failed to push logs: HTTP ${z.status} ${J}`),await t(Q)}else console.log(`[syslog-beacon] Successfully pushed ${$.length} logs`),await b0($.length)}catch(z){console.error("[syslog-beacon] Ingest error:",z),await t(z instanceof Error?z.message:String(z))}}async stop(){if(this.buffer.length>0)await this.flush()}}import{spawn as e,spawnSync as o1}from"node:child_process";import r1 from"node:dgram";import f0 from"node:fs";var t1=["/var/log/syslog","/var/log/auth.log","/var/log/messages"];function a1(){return t1.filter(($)=>{try{return f0.existsSync($)&&f0.statSync($).isFile()}catch{return!1}})}function e1(){let $=o1("journalctl",["--version"],{stdio:"ignore"});return!$.error&&$.status===0}var $$=/not seeing messages from other users/i;class $0{onLog;tailProc;udpServer;journalRestrictedHintShown=!1;constructor($){this.onLog=$}startUdpListener(){this.udpServer=r1.createSocket("udp4"),this.udpServer.on("message",($)=>{this.onLog($.toString("utf8").trim())}),this.udpServer.on("error",($)=>{console.error(`[syslog-udp] error:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { BEACON_CONFIG_REQUIRED_MESSAGE, coerceBeaconEnvironment, loadConfig, parseBeaconEnvironment, resolveBeaconApiKey, resolveSiemBeaconValidateKeyUrl, resolveIngestUrl, saveConfig, } from "./config.js";
|
|
5
|
+
import { fetchBeaconContext } from "./beacon-context.js";
|
|
6
|
+
import { fetchBeaconUpdateFromNpm } from "./beacon-update-info.js";
|
|
7
|
+
import { readBeaconCliVersion, semverLessThan } from "./beacon-version.js";
|
|
8
|
+
import { runBeacon } from "./run-beacon.js";
|
|
9
|
+
import { installService } from "./service-install.js";
|
|
10
|
+
import { probeServiceStatus } from "./service-status.js";
|
|
11
|
+
import { getStatsFilePath, readStats } from "./stats.js";
|
|
12
|
+
const BEACON_VERSION = readBeaconCliVersion();
|
|
13
|
+
const program = new Command();
|
|
14
|
+
program
|
|
15
|
+
.name("beacon")
|
|
16
|
+
.description("AgentSOC Syslog Beacon - Lightweight background log forwarder")
|
|
17
|
+
.version(BEACON_VERSION, "-v, --version");
|
|
18
|
+
program
|
|
19
|
+
.command("config")
|
|
20
|
+
.description("Configure the AgentSOC connection")
|
|
21
|
+
.option("-e, --env <env>", "Environment: production (default) or development")
|
|
22
|
+
.option("-k, --key <key>", "AgentSOC API Key")
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
if (!options.key) {
|
|
25
|
+
console.log("\n[beacon] Missing required option.\n");
|
|
26
|
+
console.log("Usage: beacon config --key <key> [--env production|development]\n");
|
|
27
|
+
console.log("Examples:");
|
|
28
|
+
console.log(" beacon config --key sk_1234567890abcdef");
|
|
29
|
+
console.log(" beacon config --key sk_1234567890abcdef --env development\n");
|
|
30
|
+
console.log("Options:");
|
|
31
|
+
console.log(" -k, --key <key> AgentSOC API Key (required)");
|
|
32
|
+
console.log(" -e, --env <env> production (default) or development; sets ingest + platform API defaults\n");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const existing = await loadConfig();
|
|
36
|
+
let environment;
|
|
37
|
+
try {
|
|
38
|
+
environment =
|
|
39
|
+
options.env !== undefined
|
|
40
|
+
? parseBeaconEnvironment(options.env)
|
|
41
|
+
: (coerceBeaconEnvironment(existing.environment) ?? "production");
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
console.error("[beacon]", e instanceof Error ? e.message : String(e));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
await saveConfig({
|
|
48
|
+
apiKey: options.key,
|
|
49
|
+
environment,
|
|
50
|
+
agentId: existing.agentId,
|
|
51
|
+
});
|
|
52
|
+
console.log("[beacon] Config saved.");
|
|
53
|
+
console.log(`[beacon] Environment: ${environment}`);
|
|
54
|
+
});
|
|
55
|
+
program
|
|
56
|
+
.command("install")
|
|
57
|
+
.description("Install the background service daemon")
|
|
58
|
+
.action(async () => {
|
|
59
|
+
try {
|
|
60
|
+
await installService();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
console.error(`[beacon] Install failed:\n\n${message}\n`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
program
|
|
69
|
+
.command("status")
|
|
70
|
+
.description("Show service state and forwarding statistics")
|
|
71
|
+
.option("--json", "Print machine-readable JSON on stdout")
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
const svc = probeServiceStatus();
|
|
74
|
+
const stats = await readStats();
|
|
75
|
+
const statsPath = getStatsFilePath();
|
|
76
|
+
const cfg = await loadConfig();
|
|
77
|
+
const apiKey = resolveBeaconApiKey(cfg);
|
|
78
|
+
const validateKeyUrl = resolveSiemBeaconValidateKeyUrl(cfg);
|
|
79
|
+
const context = apiKey
|
|
80
|
+
? await fetchBeaconContext(validateKeyUrl, apiKey)
|
|
81
|
+
: ({
|
|
82
|
+
ok: false,
|
|
83
|
+
message: BEACON_CONFIG_REQUIRED_MESSAGE,
|
|
84
|
+
});
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
console.log(JSON.stringify({
|
|
87
|
+
service: svc,
|
|
88
|
+
stats,
|
|
89
|
+
statsPath,
|
|
90
|
+
validateKeyUrl,
|
|
91
|
+
context: context.ok
|
|
92
|
+
? {
|
|
93
|
+
organizationName: context.organizationName,
|
|
94
|
+
organizationSlug: context.organizationSlug,
|
|
95
|
+
apiKeyName: context.apiKeyName,
|
|
96
|
+
}
|
|
97
|
+
: { error: context.message },
|
|
98
|
+
}, null, 2));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const labelW = 26;
|
|
102
|
+
const line = (label, value) => {
|
|
103
|
+
console.log(` ${label.padEnd(labelW)}${value}`);
|
|
104
|
+
};
|
|
105
|
+
console.log("\n AgentSOC Beacon — Status\n");
|
|
106
|
+
console.log(" Account");
|
|
107
|
+
if (context.ok) {
|
|
108
|
+
line("Organization", context.organizationName ??
|
|
109
|
+
"(name unavailable — check platform API / database)");
|
|
110
|
+
line("API key", context.apiKeyName);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
line("Connection", context.message);
|
|
114
|
+
}
|
|
115
|
+
console.log("\n Service");
|
|
116
|
+
line(`Daemon (${svc.manager})`, svc.installed ? svc.stateLabel : "not installed");
|
|
117
|
+
if (svc.active === true)
|
|
118
|
+
line("Running", "yes");
|
|
119
|
+
else if (svc.active === false)
|
|
120
|
+
line("Running", "no");
|
|
121
|
+
else
|
|
122
|
+
line("Running", "n/a");
|
|
123
|
+
if (svc.detail)
|
|
124
|
+
line("Note", svc.detail);
|
|
125
|
+
console.log("\n Forwarding (this machine)");
|
|
126
|
+
line("Log entries forwarded", String(stats.logsForwarded));
|
|
127
|
+
line("Successful batches", String(stats.batchesSucceeded));
|
|
128
|
+
line("Failed batches", String(stats.batchesFailed));
|
|
129
|
+
if (stats.updatedAt && stats.updatedAt !== new Date(0).toISOString()) {
|
|
130
|
+
line("Last stats update", stats.updatedAt);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
line("Last stats update", "(none yet)");
|
|
134
|
+
}
|
|
135
|
+
if (stats.lastError) {
|
|
136
|
+
line("Last error", stats.lastError);
|
|
137
|
+
}
|
|
138
|
+
console.log();
|
|
139
|
+
});
|
|
140
|
+
program
|
|
141
|
+
.command("update")
|
|
142
|
+
.description("Check for a newer Beacon CLI and optionally install it")
|
|
143
|
+
.option("--json", "Print machine-readable JSON on stdout")
|
|
144
|
+
.option("-y, --yes", "Run npm install -g @agentsoc/beacon@latest (non-interactive upgrade)")
|
|
145
|
+
.action(async (opts) => {
|
|
146
|
+
const current = BEACON_VERSION;
|
|
147
|
+
const fetched = await fetchBeaconUpdateFromNpm();
|
|
148
|
+
if (opts.json) {
|
|
149
|
+
const latest = fetched.ok ? fetched.info.latestVersion : null;
|
|
150
|
+
const outdated = latest != null &&
|
|
151
|
+
latest.length > 0 &&
|
|
152
|
+
semverLessThan(current, latest);
|
|
153
|
+
console.log(JSON.stringify({
|
|
154
|
+
currentVersion: current,
|
|
155
|
+
registry: "https://registry.npmjs.org/@agentsoc%2fbeacon",
|
|
156
|
+
remote: fetched.ok ? fetched.info : null,
|
|
157
|
+
error: fetched.ok ? undefined : fetched.message,
|
|
158
|
+
updateAvailable: fetched.ok ? outdated : null,
|
|
159
|
+
}, null, 2));
|
|
160
|
+
if (!fetched.ok)
|
|
161
|
+
process.exit(1);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!fetched.ok) {
|
|
165
|
+
console.error(`[beacon] Could not reach npm registry: ${fetched.message}`);
|
|
166
|
+
console.error("[beacon] You can still run: npm install -g @agentsoc/beacon@latest");
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const { info } = fetched;
|
|
170
|
+
const latest = info.latestVersion;
|
|
171
|
+
console.log("\n AgentSOC Beacon — Update check\n");
|
|
172
|
+
console.log(` This install ${current}`);
|
|
173
|
+
if (latest) {
|
|
174
|
+
console.log(` Latest on registry ${latest}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(" Latest on registry (unavailable — unexpected npm response)");
|
|
178
|
+
}
|
|
179
|
+
const canCompare = Boolean(latest && latest.length > 0);
|
|
180
|
+
const outdated = canCompare && semverLessThan(current, latest);
|
|
181
|
+
if (canCompare && !outdated) {
|
|
182
|
+
console.log("\n You are on the latest published version.\n");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (canCompare && outdated) {
|
|
186
|
+
console.log("\n A newer version is available.\n");
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log("\n Compare versions manually, or reinstall with:\n");
|
|
190
|
+
}
|
|
191
|
+
console.log(` ${info.install.npm}`);
|
|
192
|
+
console.log(` ${info.install.bun}`);
|
|
193
|
+
console.log(` ${info.install.pnpm}`);
|
|
194
|
+
if (info.docsUrl) {
|
|
195
|
+
console.log(`\n Docs: ${info.docsUrl}`);
|
|
196
|
+
}
|
|
197
|
+
if (opts.yes) {
|
|
198
|
+
if (!outdated && canCompare) {
|
|
199
|
+
console.log("\n[beacon] Already up to date — skipping install.\n");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
console.log("\n[beacon] Running npm install -g @agentsoc/beacon@latest …\n");
|
|
203
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
204
|
+
try {
|
|
205
|
+
execFileSync(npm, ["install", "-g", "@agentsoc/beacon@latest"], { stdio: "inherit", env: process.env });
|
|
206
|
+
console.log("\n[beacon] Update finished. Run `beacon --version` to verify.\n");
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.log("\n To upgrade now, run the same command with --yes\n");
|
|
214
|
+
});
|
|
215
|
+
program
|
|
216
|
+
.command("run")
|
|
217
|
+
.description("Run the forwarder daemon (foreground)")
|
|
218
|
+
.option("--batch-size <n>", "Batch size", "25")
|
|
219
|
+
.option("--flush-ms <n>", "Flush interval in ms", "2000")
|
|
220
|
+
.action(async (options) => {
|
|
221
|
+
const config = await loadConfig();
|
|
222
|
+
const apiKey = resolveBeaconApiKey(config);
|
|
223
|
+
const ingestUrl = resolveIngestUrl(config);
|
|
224
|
+
if (!apiKey) {
|
|
225
|
+
console.error(`[beacon] ${BEACON_CONFIG_REQUIRED_MESSAGE}`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
console.log("[beacon] Starting in foreground...");
|
|
229
|
+
const beacon = await runBeacon({
|
|
230
|
+
apiKey,
|
|
231
|
+
ingestUrl,
|
|
232
|
+
batchSize: parseInt(options.batchSize, 10),
|
|
233
|
+
flushMs: parseInt(options.flushMs, 10),
|
|
234
|
+
});
|
|
235
|
+
const stop = async () => {
|
|
236
|
+
console.log("\n[beacon] Stopping...");
|
|
237
|
+
await beacon.stop();
|
|
238
|
+
process.exit(0);
|
|
239
|
+
};
|
|
240
|
+
process.on("SIGINT", stop);
|
|
241
|
+
process.on("SIGTERM", stop);
|
|
242
|
+
});
|
|
243
|
+
program.parse(process.argv);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-install.d.ts","sourceRoot":"","sources":["../src/service-install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service-install.d.ts","sourceRoot":"","sources":["../src/service-install.ts"],"names":[],"mappings":"AAkJA,wBAAsB,cAAc,kBA4EnC"}
|
package/dist/service-install.js
CHANGED
|
@@ -4,11 +4,17 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { execFileSync, execSync } from 'node:child_process';
|
|
6
6
|
import { BEACON_CONFIG_REQUIRED_MESSAGE, loadConfig } from './config.js';
|
|
7
|
-
const PERMISSION_DENIED_HINT = `That location is only writable as root.
|
|
7
|
+
const PERMISSION_DENIED_HINT = `That location is only writable as root. From the same shell where \`beacon\` works, run:
|
|
8
8
|
|
|
9
|
-
sudo beacon install
|
|
9
|
+
sudo "$(command -v beacon)" install
|
|
10
10
|
|
|
11
|
-
You will be prompted for your administrator password
|
|
11
|
+
You will be prompted for your administrator password.
|
|
12
|
+
|
|
13
|
+
Why not \`sudo beacon install\`? sudo uses a minimal PATH. When beacon was installed with npm/nvm/fnm as your user, root often cannot find the \`beacon\` command (you will see "command not found"). The line above resolves the full path before sudo runs.
|
|
14
|
+
|
|
15
|
+
Other options:
|
|
16
|
+
sudo env "PATH=$PATH" beacon install
|
|
17
|
+
sudo /path/from/which/beacon install`;
|
|
12
18
|
function permissionDeniedInstallError(destPath) {
|
|
13
19
|
return new Error(`Cannot install the system service: permission denied for ${path.dirname(destPath)}.\n\n${PERMISSION_DENIED_HINT}`);
|
|
14
20
|
}
|
|
@@ -123,7 +129,7 @@ function launchctlBootstrapSystem(plistPath) {
|
|
|
123
129
|
const err = e;
|
|
124
130
|
const detail = err.stderr?.toString('utf8').trim() ?? '';
|
|
125
131
|
const suffix = detail ? `\n${detail}` : '';
|
|
126
|
-
throw new Error(`launchctl bootstrap failed for ${plistPath}.${suffix}\n\nTry:\n sudo launchctl bootout system ${plistPath}\n sudo beacon install`);
|
|
132
|
+
throw new Error(`launchctl bootstrap failed for ${plistPath}.${suffix}\n\nTry:\n sudo launchctl bootout system ${plistPath}\n sudo "$(command -v beacon)" install`);
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
export async function installService() {
|
package/dist/tail.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ export declare class SyslogSource {
|
|
|
3
3
|
private onLog;
|
|
4
4
|
private tailProc?;
|
|
5
5
|
private udpServer?;
|
|
6
|
+
/** Avoid repeating the tip if stderr arrives in multiple chunks. */
|
|
7
|
+
private journalRestrictedHintShown;
|
|
6
8
|
constructor(onLog: LogCallback);
|
|
9
|
+
private startUdpListener;
|
|
7
10
|
start(): void;
|
|
8
11
|
stop(): void;
|
|
9
12
|
}
|
package/dist/tail.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tail.d.ts","sourceRoot":"","sources":["../src/tail.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tail.d.ts","sourceRoot":"","sources":["../src/tail.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAgCjD,qBAAa,YAAY;IAMX,OAAO,CAAC,KAAK;IALzB,OAAO,CAAC,QAAQ,CAAC,CAAe;IAChC,OAAO,CAAC,SAAS,CAAC,CAAe;IACjC,oEAAoE;IACpE,OAAO,CAAC,0BAA0B,CAAS;gBAEvB,KAAK,EAAE,WAAW;IAEtC,OAAO,CAAC,gBAAgB;IAcjB,KAAK;IAqEL,IAAI;CAQZ"}
|
package/dist/tail.js
CHANGED
|
@@ -1,19 +1,79 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import dgram from "node:dgram";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
/**
|
|
5
|
+
* Fallback when journalctl is unavailable (non-systemd or embedded images).
|
|
6
|
+
* Debian/Ubuntu rsyslog paths and common /var/log/messages.
|
|
7
|
+
*/
|
|
8
|
+
const LINUX_FILE_LOG_CANDIDATES = [
|
|
9
|
+
"/var/log/syslog",
|
|
10
|
+
"/var/log/auth.log",
|
|
11
|
+
"/var/log/messages",
|
|
12
|
+
];
|
|
13
|
+
function existingLinuxLogFiles() {
|
|
14
|
+
return LINUX_FILE_LOG_CANDIDATES.filter((p) => {
|
|
15
|
+
try {
|
|
16
|
+
return fs.existsSync(p) && fs.statSync(p).isFile();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** True if systemd-journald CLI is on PATH (standard on systemd Linux; no extra packages). */
|
|
24
|
+
function journalctlAvailable() {
|
|
25
|
+
const r = spawnSync("journalctl", ["--version"], { stdio: "ignore" });
|
|
26
|
+
return !r.error && r.status === 0;
|
|
27
|
+
}
|
|
28
|
+
/** journalctl prints this when the user lacks system journal ACLs (first follow only). */
|
|
29
|
+
const JOURNAL_RESTRICTED_HINT = /not seeing messages from other users/i;
|
|
3
30
|
export class SyslogSource {
|
|
4
31
|
onLog;
|
|
5
32
|
tailProc;
|
|
6
33
|
udpServer;
|
|
34
|
+
/** Avoid repeating the tip if stderr arrives in multiple chunks. */
|
|
35
|
+
journalRestrictedHintShown = false;
|
|
7
36
|
constructor(onLog) {
|
|
8
37
|
this.onLog = onLog;
|
|
9
38
|
}
|
|
39
|
+
startUdpListener() {
|
|
40
|
+
this.udpServer = dgram.createSocket("udp4");
|
|
41
|
+
this.udpServer.on("message", (msg) => {
|
|
42
|
+
this.onLog(msg.toString("utf8").trim());
|
|
43
|
+
});
|
|
44
|
+
this.udpServer.on("error", (err) => {
|
|
45
|
+
console.error(`[syslog-udp] error:\n${err.stack}`);
|
|
46
|
+
this.udpServer?.close();
|
|
47
|
+
});
|
|
48
|
+
this.udpServer.bind(5140, () => {
|
|
49
|
+
console.log("[syslog-udp] listening on UDP 5140");
|
|
50
|
+
});
|
|
51
|
+
}
|
|
10
52
|
start() {
|
|
11
53
|
if (process.platform === "linux") {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
54
|
+
let stderrIsJournalctl = false;
|
|
55
|
+
if (journalctlAvailable()) {
|
|
56
|
+
stderrIsJournalctl = true;
|
|
57
|
+
console.log("[syslog-tail] Streaming systemd journal (journalctl -f)");
|
|
58
|
+
this.tailProc = spawn("journalctl", [
|
|
59
|
+
"-f",
|
|
60
|
+
"--no-pager",
|
|
61
|
+
"-o",
|
|
62
|
+
"short-iso",
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const logFiles = existingLinuxLogFiles();
|
|
67
|
+
if (logFiles.length > 0) {
|
|
68
|
+
console.log(`[syslog-tail] Following: ${logFiles.join(", ")}`);
|
|
69
|
+
this.tailProc = spawn("tail", ["-F", ...logFiles]);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log("[syslog-tail] No journalctl or log files under /var/log; listening on UDP 5140");
|
|
73
|
+
this.startUdpListener();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
17
77
|
this.tailProc.stdout?.on("data", (data) => {
|
|
18
78
|
const lines = data.toString().split("\n");
|
|
19
79
|
for (const line of lines) {
|
|
@@ -22,7 +82,14 @@ export class SyslogSource {
|
|
|
22
82
|
}
|
|
23
83
|
});
|
|
24
84
|
this.tailProc.stderr?.on("data", (data) => {
|
|
25
|
-
|
|
85
|
+
const text = data.toString();
|
|
86
|
+
console.error(`[syslog-tail] ${text}`);
|
|
87
|
+
if (stderrIsJournalctl &&
|
|
88
|
+
!this.journalRestrictedHintShown &&
|
|
89
|
+
JOURNAL_RESTRICTED_HINT.test(text)) {
|
|
90
|
+
this.journalRestrictedHintShown = true;
|
|
91
|
+
console.error("[syslog-tail] Only your user journal is visible. For system-wide logs, add this user to group systemd-journal or adm, or run beacon as root (no extra packages).");
|
|
92
|
+
}
|
|
26
93
|
});
|
|
27
94
|
}
|
|
28
95
|
else if (process.platform === "darwin") {
|
|
@@ -46,18 +113,8 @@ export class SyslogSource {
|
|
|
46
113
|
});
|
|
47
114
|
}
|
|
48
115
|
else {
|
|
49
|
-
// Windows or other
|
|
50
|
-
this.
|
|
51
|
-
this.udpServer.on("message", (msg) => {
|
|
52
|
-
this.onLog(msg.toString("utf8").trim());
|
|
53
|
-
});
|
|
54
|
-
this.udpServer.on("error", (err) => {
|
|
55
|
-
console.error(`[syslog-udp] error:\n${err.stack}`);
|
|
56
|
-
this.udpServer?.close();
|
|
57
|
-
});
|
|
58
|
-
this.udpServer.bind(5140, () => {
|
|
59
|
-
console.log("[syslog-udp] listening on UDP 5140");
|
|
60
|
-
});
|
|
116
|
+
// Windows or other — UDP listener for forwarded syslog
|
|
117
|
+
this.startUdpListener();
|
|
61
118
|
}
|
|
62
119
|
}
|
|
63
120
|
stop() {
|