@scanoss_test/sdk 2.0.1
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/bin/.gitkeep +0 -0
- package/dist/index.d.mts +444 -0
- package/dist/index.d.ts +444 -0
- package/dist/index.js +320 -0
- package/dist/index.mjs +298 -0
- package/package.json +67 -0
- package/src/binary.ts +57 -0
- package/src/client.ts +327 -0
- package/src/errors.ts +22 -0
- package/src/gen/.gitkeep +0 -0
- package/src/gen/scanoss/v1/commands_pb.ts +537 -0
- package/src/gen/scanoss/v1/enums_pb.ts +84 -0
- package/src/gen/scanoss/v1/types_pb.ts +191 -0
- package/src/index.ts +9 -0
- package/src/types.ts +98 -0
package/src/binary.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary resolution for SCANOSS daemon.
|
|
3
|
+
*
|
|
4
|
+
* The binary is provided by platform-specific packages (@scanoss_test/linux-x64, etc.).
|
|
5
|
+
* npm will automatically install the correct one based on the user's OS and CPU.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
const PLATFORMS = [
|
|
12
|
+
'@scanoss_test/linux-x64',
|
|
13
|
+
'@scanoss_test/linux-arm64',
|
|
14
|
+
'@scanoss_test/darwin-x64',
|
|
15
|
+
'@scanoss_test/darwin-arm64',
|
|
16
|
+
'@scanoss_test/win32-x64',
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the path to the SCANOSS binary.
|
|
21
|
+
*
|
|
22
|
+
* @returns Path to the scanoss binary
|
|
23
|
+
* @throws Error if no binary package is installed
|
|
24
|
+
*/
|
|
25
|
+
export function getBinaryPath(): string {
|
|
26
|
+
// Try each platform package - npm only installs the matching one
|
|
27
|
+
for (const platform of PLATFORMS) {
|
|
28
|
+
try {
|
|
29
|
+
const binaryPath = require(platform);
|
|
30
|
+
if (existsSync(binaryPath)) {
|
|
31
|
+
return binaryPath;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Package not installed for this platform, try next
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback: check if binary is in PATH
|
|
39
|
+
try {
|
|
40
|
+
const pathBinary = execSync('which scanoss', { encoding: 'utf-8' }).trim();
|
|
41
|
+
if (pathBinary && existsSync(pathBinary)) {
|
|
42
|
+
return pathBinary;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Not in PATH
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(
|
|
49
|
+
'SCANOSS binary not found. The platform-specific package should be installed automatically.\n' +
|
|
50
|
+
'If not, install it manually:\n' +
|
|
51
|
+
' npm install @scanoss_test/linux-x64 # Linux x64\n' +
|
|
52
|
+
' npm install @scanoss_test/linux-arm64 # Linux ARM64\n' +
|
|
53
|
+
' npm install @scanoss_test/darwin-x64 # macOS Intel\n' +
|
|
54
|
+
' npm install @scanoss_test/darwin-arm64 # macOS Apple Silicon\n' +
|
|
55
|
+
' npm install @scanoss_test/win32-x64 # Windows x64'
|
|
56
|
+
);
|
|
57
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCANOSS SDK client using protobuf binary protocol
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
6
|
+
import { create, toBinary, fromBinary } from '@bufbuild/protobuf';
|
|
7
|
+
import { getBinaryPath } from './binary';
|
|
8
|
+
import { ScanossError } from './errors';
|
|
9
|
+
import {
|
|
10
|
+
RequestSchema,
|
|
11
|
+
ResponseSchema,
|
|
12
|
+
ScanCommandSchema,
|
|
13
|
+
FilterFilesCommandSchema,
|
|
14
|
+
FingerprintCommandSchema,
|
|
15
|
+
GenerateSbomCommandSchema,
|
|
16
|
+
VersionCommandSchema,
|
|
17
|
+
} from './gen/scanoss/v1/commands_pb';
|
|
18
|
+
import type {
|
|
19
|
+
Request,
|
|
20
|
+
Response,
|
|
21
|
+
ScanResult,
|
|
22
|
+
FilterFilesResult,
|
|
23
|
+
FingerprintResult,
|
|
24
|
+
GenerateSbomResult,
|
|
25
|
+
VersionResult,
|
|
26
|
+
} from './gen/scanoss/v1/commands_pb';
|
|
27
|
+
import { OutputFormat, ScanType } from './gen/scanoss/v1/enums_pb';
|
|
28
|
+
import type {
|
|
29
|
+
ScanParams,
|
|
30
|
+
FilterFilesParams,
|
|
31
|
+
FingerprintParams,
|
|
32
|
+
GenerateSbomParams,
|
|
33
|
+
} from './types';
|
|
34
|
+
|
|
35
|
+
/** Options for creating a Scanoss instance */
|
|
36
|
+
export interface ScanossOptions {
|
|
37
|
+
/** Path to the scanoss binary. If not provided, will be auto-detected. */
|
|
38
|
+
binaryPath?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PendingRequest {
|
|
42
|
+
resolve: (value: Response) => void;
|
|
43
|
+
reject: (reason: Error) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* SCANOSS SDK client.
|
|
48
|
+
*
|
|
49
|
+
* This client communicates with the SCANOSS daemon process via stdin/stdout
|
|
50
|
+
* using length-prefixed protobuf binary protocol.
|
|
51
|
+
*
|
|
52
|
+
* Wire format:
|
|
53
|
+
* ```
|
|
54
|
+
* ┌──────────────────┬─────────────────────────┐
|
|
55
|
+
* │ 4 bytes (BE) │ N bytes │
|
|
56
|
+
* │ message length │ protobuf binary data │
|
|
57
|
+
* └──────────────────┴─────────────────────────┘
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const scanoss = new Scanoss();
|
|
63
|
+
* const result = await scanoss.scan({ path: '/path/to/project' });
|
|
64
|
+
* console.log(result);
|
|
65
|
+
* await scanoss.close();
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export class Scanoss {
|
|
69
|
+
private process: ChildProcess | null = null;
|
|
70
|
+
private requestId = 0;
|
|
71
|
+
private pending = new Map<number, PendingRequest>();
|
|
72
|
+
private binaryPath: string;
|
|
73
|
+
private closed = false;
|
|
74
|
+
private buffer: Buffer = Buffer.alloc(0);
|
|
75
|
+
private expectedLength: number | null = null;
|
|
76
|
+
|
|
77
|
+
constructor(options: ScanossOptions = {}) {
|
|
78
|
+
this.binaryPath = options.binaryPath ?? getBinaryPath();
|
|
79
|
+
this.start();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private start(): void {
|
|
83
|
+
this.process = spawn(this.binaryPath, ['daemon'], {
|
|
84
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Handle binary data from stdout
|
|
88
|
+
this.process.stdout!.on('data', (chunk: Buffer) => {
|
|
89
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
90
|
+
this.processBuffer();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.process.on('error', (err) => {
|
|
94
|
+
console.error('Daemon process error:', err);
|
|
95
|
+
this.rejectAllPending(err);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.process.on('exit', (code) => {
|
|
99
|
+
if (!this.closed) {
|
|
100
|
+
const err = new Error(`Daemon process exited unexpectedly with code ${code}`);
|
|
101
|
+
this.rejectAllPending(err);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private processBuffer(): void {
|
|
107
|
+
while (true) {
|
|
108
|
+
// Read length prefix if we don't have it
|
|
109
|
+
if (this.expectedLength === null) {
|
|
110
|
+
if (this.buffer.length < 4) {
|
|
111
|
+
return; // Need more data
|
|
112
|
+
}
|
|
113
|
+
this.expectedLength = this.buffer.readUInt32BE(0);
|
|
114
|
+
this.buffer = this.buffer.subarray(4);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Read message if we have enough data
|
|
118
|
+
if (this.buffer.length < this.expectedLength) {
|
|
119
|
+
return; // Need more data
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const messageData = this.buffer.subarray(0, this.expectedLength);
|
|
123
|
+
this.buffer = this.buffer.subarray(this.expectedLength);
|
|
124
|
+
this.expectedLength = null;
|
|
125
|
+
|
|
126
|
+
// Parse response
|
|
127
|
+
try {
|
|
128
|
+
const response = fromBinary(ResponseSchema, messageData);
|
|
129
|
+
const pending = this.pending.get(response.id);
|
|
130
|
+
if (pending) {
|
|
131
|
+
if (response.result.case === 'error') {
|
|
132
|
+
pending.reject(new ScanossError(
|
|
133
|
+
response.result.value.code,
|
|
134
|
+
response.result.value.message
|
|
135
|
+
));
|
|
136
|
+
} else {
|
|
137
|
+
pending.resolve(response);
|
|
138
|
+
}
|
|
139
|
+
this.pending.delete(response.id);
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error('Failed to parse daemon response:', err);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private rejectAllPending(err: Error): void {
|
|
148
|
+
for (const pending of this.pending.values()) {
|
|
149
|
+
pending.reject(err);
|
|
150
|
+
}
|
|
151
|
+
this.pending.clear();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private send(request: Request): Promise<Response> {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
if (!this.process || this.closed) {
|
|
157
|
+
reject(new Error('Daemon process is not running'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.pending.set(request.id, { resolve, reject });
|
|
162
|
+
|
|
163
|
+
// Serialize to binary
|
|
164
|
+
const data = toBinary(RequestSchema, request);
|
|
165
|
+
|
|
166
|
+
// Write length prefix (4 bytes, big-endian)
|
|
167
|
+
const lengthBuf = Buffer.alloc(4);
|
|
168
|
+
lengthBuf.writeUInt32BE(data.length);
|
|
169
|
+
|
|
170
|
+
this.process.stdin!.write(lengthBuf);
|
|
171
|
+
this.process.stdin!.write(data);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Scan files for open source matches.
|
|
177
|
+
*/
|
|
178
|
+
async scan(params: ScanParams): Promise<ScanResult> {
|
|
179
|
+
const id = ++this.requestId;
|
|
180
|
+
|
|
181
|
+
const scanCmd = create(ScanCommandSchema, {
|
|
182
|
+
path: params.path,
|
|
183
|
+
format: formatToEnum(params.format ?? 'json'),
|
|
184
|
+
scanType: scanTypeToEnum(params.scanType ?? 'identify'),
|
|
185
|
+
include: params.include ?? [],
|
|
186
|
+
exclude: params.exclude ?? [],
|
|
187
|
+
apiUrl: params.apiUrl,
|
|
188
|
+
apiKey: params.apiKey,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const request = create(RequestSchema, {
|
|
192
|
+
id,
|
|
193
|
+
command: { case: 'scan', value: scanCmd },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const response = await this.send(request);
|
|
197
|
+
if (response.result.case !== 'scan') {
|
|
198
|
+
throw new Error('Unexpected response type');
|
|
199
|
+
}
|
|
200
|
+
return response.result.value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Filter files in a directory.
|
|
205
|
+
*/
|
|
206
|
+
async filterFiles(params: FilterFilesParams): Promise<FilterFilesResult> {
|
|
207
|
+
const id = ++this.requestId;
|
|
208
|
+
|
|
209
|
+
const cmd = create(FilterFilesCommandSchema, {
|
|
210
|
+
path: params.path,
|
|
211
|
+
include: params.include ?? [],
|
|
212
|
+
exclude: params.exclude ?? [],
|
|
213
|
+
skipHidden: params.skipHidden ?? true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const request = create(RequestSchema, {
|
|
217
|
+
id,
|
|
218
|
+
command: { case: 'filterFiles', value: cmd },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const response = await this.send(request);
|
|
222
|
+
if (response.result.case !== 'filterFiles') {
|
|
223
|
+
throw new Error('Unexpected response type');
|
|
224
|
+
}
|
|
225
|
+
return response.result.value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Generate WFP fingerprints for files.
|
|
230
|
+
*/
|
|
231
|
+
async fingerprint(params: FingerprintParams): Promise<FingerprintResult> {
|
|
232
|
+
const id = ++this.requestId;
|
|
233
|
+
|
|
234
|
+
const cmd = create(FingerprintCommandSchema, {
|
|
235
|
+
path: params.path,
|
|
236
|
+
include: params.include ?? [],
|
|
237
|
+
exclude: params.exclude ?? [],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const request = create(RequestSchema, {
|
|
241
|
+
id,
|
|
242
|
+
command: { case: 'fingerprint', value: cmd },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const response = await this.send(request);
|
|
246
|
+
if (response.result.case !== 'fingerprint') {
|
|
247
|
+
throw new Error('Unexpected response type');
|
|
248
|
+
}
|
|
249
|
+
return response.result.value;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Generate SBOM from scan results.
|
|
254
|
+
*/
|
|
255
|
+
async generateSbom(params: GenerateSbomParams): Promise<GenerateSbomResult> {
|
|
256
|
+
const id = ++this.requestId;
|
|
257
|
+
|
|
258
|
+
const cmd = create(GenerateSbomCommandSchema, {
|
|
259
|
+
inputPath: params.path,
|
|
260
|
+
format: formatToEnum(params.format ?? 'spdx'),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const request = create(RequestSchema, {
|
|
264
|
+
id,
|
|
265
|
+
command: { case: 'generateSbom', value: cmd },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const response = await this.send(request);
|
|
269
|
+
if (response.result.case !== 'generateSbom') {
|
|
270
|
+
throw new Error('Unexpected response type');
|
|
271
|
+
}
|
|
272
|
+
return response.result.value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get version information.
|
|
277
|
+
*/
|
|
278
|
+
async version(): Promise<VersionResult> {
|
|
279
|
+
const id = ++this.requestId;
|
|
280
|
+
|
|
281
|
+
const cmd = create(VersionCommandSchema, {});
|
|
282
|
+
|
|
283
|
+
const request = create(RequestSchema, {
|
|
284
|
+
id,
|
|
285
|
+
command: { case: 'version', value: cmd },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const response = await this.send(request);
|
|
289
|
+
if (response.result.case !== 'version') {
|
|
290
|
+
throw new Error('Unexpected response type');
|
|
291
|
+
}
|
|
292
|
+
return response.result.value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Close the daemon process.
|
|
297
|
+
*/
|
|
298
|
+
async close(): Promise<void> {
|
|
299
|
+
this.closed = true;
|
|
300
|
+
|
|
301
|
+
if (this.process) {
|
|
302
|
+
this.process.kill();
|
|
303
|
+
this.process = null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.pending.clear();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function formatToEnum(format: string): OutputFormat {
|
|
311
|
+
const formats: Record<string, OutputFormat> = {
|
|
312
|
+
json: OutputFormat.JSON,
|
|
313
|
+
spdx: OutputFormat.SPDX,
|
|
314
|
+
cyclonedx: OutputFormat.CYCLONEDX,
|
|
315
|
+
csv: OutputFormat.CSV,
|
|
316
|
+
wfp: OutputFormat.WFP,
|
|
317
|
+
};
|
|
318
|
+
return formats[format.toLowerCase()] ?? OutputFormat.JSON;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function scanTypeToEnum(scanType: string): ScanType {
|
|
322
|
+
const types: Record<string, ScanType> = {
|
|
323
|
+
identify: ScanType.IDENTIFY,
|
|
324
|
+
blacklist: ScanType.BLACKLIST,
|
|
325
|
+
};
|
|
326
|
+
return types[scanType.toLowerCase()] ?? ScanType.IDENTIFY;
|
|
327
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types for SCANOSS SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error thrown by SCANOSS operations
|
|
7
|
+
*/
|
|
8
|
+
export class ScanossError extends Error {
|
|
9
|
+
/** Error code from the daemon */
|
|
10
|
+
readonly code: number;
|
|
11
|
+
|
|
12
|
+
constructor(code: number, message: string) {
|
|
13
|
+
super(`[${code}] ${message}`);
|
|
14
|
+
this.name = 'ScanossError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
|
|
17
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
18
|
+
if (Error.captureStackTrace) {
|
|
19
|
+
Error.captureStackTrace(this, ScanossError);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/gen/.gitkeep
ADDED
|
File without changes
|