cryptoserve 0.1.3 → 0.2.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.
package/lib/scanner.mjs CHANGED
@@ -1,47 +1,30 @@
1
1
  /**
2
- * JavaScript/TypeScript crypto dependency and secret scanner.
2
+ * Cryptographic dependency and secret scanner — orchestrates all sub-scanners.
3
3
  *
4
4
  * Scans projects for:
5
- * 1. Cryptographic dependencies (package.json, imports, algorithm strings)
6
- * 2. Hardcoded secrets (API keys, passwords patterns from secretless-ai)
7
- * 3. Certificate/key files (.pem, .key, .crt, .p12)
5
+ * 1. Cryptographic dependencies (package.json, go.mod, requirements.txt, etc.)
6
+ * 2. Source code crypto patterns (JS/TS, Go, Python, Java, Rust, C/C++)
7
+ * 3. Hardcoded secrets (API keys, passwords — patterns from secretless-ai)
8
+ * 4. Certificate/key files (.pem, .key, .crt, .p12)
9
+ * 5. TLS version issues in config files
10
+ * 6. Binary crypto signatures (optional, via --binary flag)
8
11
  *
9
12
  * Output matches the library inventory format used by pqc-engine.mjs.
10
13
  * Zero dependencies — uses only node:fs and node:path.
11
14
  */
12
15
 
13
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
16
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
14
17
  import { join, relative, extname, basename } from 'node:path';
18
+ import { LANGUAGE_PATTERNS, scanSourceFile, detectLanguage, MULTI_LANG_EXTENSIONS } from './scanner-languages.mjs';
19
+ import { scanManifests } from './scanner-manifests.mjs';
20
+ import { scanTlsConfigs } from './scanner-tls.mjs';
21
+ import { lookupAlgorithm } from './algorithm-db.mjs';
22
+ import { lookupNpmPackage } from './crypto-registry.mjs';
23
+ import { walkProject } from './walker.mjs';
24
+ import { loadScannerConfig } from './config.mjs';
15
25
 
16
26
  // ---------------------------------------------------------------------------
17
- // Known crypto packages algorithm mappings
18
- // ---------------------------------------------------------------------------
19
-
20
- const CRYPTO_PACKAGES = {
21
- 'crypto-js': { algorithms: ['AES', 'DES', '3DES', 'MD5', 'SHA-256', 'SHA-512', 'HMAC'], quantumRisk: 'low', category: 'symmetric' },
22
- 'bcrypt': { algorithms: ['bcrypt'], quantumRisk: 'none', category: 'kdf' },
23
- 'bcryptjs': { algorithms: ['bcrypt'], quantumRisk: 'none', category: 'kdf' },
24
- 'jsonwebtoken': { algorithms: ['RS256', 'HS256', 'ES256'], quantumRisk: 'high', category: 'token' },
25
- 'jose': { algorithms: ['RS256', 'ES256', 'EdDSA', 'AES-GCM'], quantumRisk: 'high', category: 'token' },
26
- 'node-forge': { algorithms: ['RSA', 'AES', 'SHA-256', 'HMAC', 'TLS'], quantumRisk: 'high', category: 'tls' },
27
- 'tweetnacl': { algorithms: ['X25519', 'Ed25519', 'XSalsa20'], quantumRisk: 'high', category: 'asymmetric' },
28
- 'libsodium-wrappers': { algorithms: ['X25519', 'Ed25519', 'ChaCha20', 'AES-256-GCM'], quantumRisk: 'high', category: 'asymmetric' },
29
- '@noble/curves': { algorithms: ['ECDSA', 'Ed25519', 'X25519'], quantumRisk: 'high', category: 'asymmetric' },
30
- '@noble/hashes': { algorithms: ['SHA-256', 'SHA-512', 'SHA3', 'Blake2'], quantumRisk: 'low', category: 'hash' },
31
- '@noble/post-quantum': { algorithms: ['ML-KEM', 'ML-DSA', 'SLH-DSA'], quantumRisk: 'none', category: 'pqc' },
32
- 'openpgp': { algorithms: ['RSA', 'ECDSA', 'AES', 'SHA-256'], quantumRisk: 'high', category: 'asymmetric' },
33
- 'elliptic': { algorithms: ['ECDSA', 'ECDHE', 'Ed25519'], quantumRisk: 'high', category: 'asymmetric' },
34
- 'secp256k1': { algorithms: ['ECDSA'], quantumRisk: 'high', category: 'asymmetric' },
35
- 'argon2': { algorithms: ['Argon2'], quantumRisk: 'none', category: 'kdf' },
36
- 'scrypt': { algorithms: ['scrypt'], quantumRisk: 'none', category: 'kdf' },
37
- 'pbkdf2': { algorithms: ['PBKDF2'], quantumRisk: 'none', category: 'kdf' },
38
- 'tls': { algorithms: ['TLS', 'RSA', 'ECDSA'], quantumRisk: 'high', category: 'tls' },
39
- 'ssh2': { algorithms: ['RSA', 'Ed25519', 'ECDSA', 'AES'], quantumRisk: 'high', category: 'asymmetric' },
40
- 'node-rsa': { algorithms: ['RSA'], quantumRisk: 'high', category: 'asymmetric' },
41
- };
42
-
43
- // ---------------------------------------------------------------------------
44
- // Import/require patterns to detect in source code
27
+ // Import/require patterns to detect in JS/TS source code
45
28
  // ---------------------------------------------------------------------------
46
29
 
47
30
  const IMPORT_PATTERNS = [
@@ -117,71 +100,29 @@ const SECRET_PATTERNS = [
117
100
  // File patterns for cert/key discovery
118
101
  const CERT_EXTENSIONS = new Set(['.pem', '.key', '.crt', '.p12', '.pfx', '.jks', '.keystore']);
119
102
 
120
- // ---------------------------------------------------------------------------
121
- // File walker
122
- // ---------------------------------------------------------------------------
123
-
124
- const SKIP_DIRS = new Set([
125
- 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
126
- '.cache', '.nuxt', '.output', '.svelte-kit', '__pycache__',
127
- 'vendor', '.venv', 'venv',
128
- ]);
129
-
130
- const SOURCE_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']);
131
-
132
- function walkFiles(dir, maxFiles = 10000, maxBytes = 500 * 1024 * 1024) {
133
- const files = [];
134
- let totalBytes = 0;
135
-
136
- function walk(currentDir) {
137
- if (files.length >= maxFiles || totalBytes >= maxBytes) return;
138
-
139
- let entries;
140
- try { entries = readdirSync(currentDir, { withFileTypes: true }); }
141
- catch { return; }
142
-
143
- for (const entry of entries) {
144
- if (files.length >= maxFiles || totalBytes >= maxBytes) return;
145
-
146
- if (entry.isDirectory()) {
147
- if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
148
- walk(join(currentDir, entry.name));
149
- }
150
- continue;
151
- }
152
-
153
- if (!entry.isFile()) continue;
154
-
155
- const filePath = join(currentDir, entry.name);
156
- try {
157
- const stat = statSync(filePath);
158
- if (stat.size > 1024 * 1024) continue; // Skip files >1MB
159
- totalBytes += stat.size;
160
- files.push(filePath);
161
- } catch { continue; }
162
- }
163
- }
164
-
165
- walk(dir);
166
- return files;
167
- }
103
+ const JS_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']);
168
104
 
169
105
  // ---------------------------------------------------------------------------
170
106
  // Scanner
171
107
  // ---------------------------------------------------------------------------
172
108
 
173
- export function scanProject(projectDir) {
109
+ export function scanProject(projectDir, options = {}) {
174
110
  const results = {
175
111
  libraries: [],
176
112
  secrets: [],
177
113
  weakPatterns: [],
178
114
  certFiles: [],
179
115
  filesScanned: 0,
116
+ // New in v0.2.0
117
+ sourceAlgorithms: [],
118
+ tlsFindings: [],
119
+ binaryFindings: [],
120
+ languagesDetected: new Set(),
121
+ manifestsFound: [],
180
122
  };
181
123
 
182
124
  // 1. Scan package.json for crypto dependencies (root + monorepo workspaces)
183
125
  const pkgPaths = [join(projectDir, 'package.json')];
184
- // Check common monorepo locations for nested package.json files
185
126
  const monorepoGlobs = ['apps', 'packages', 'libs', 'modules', 'services'];
186
127
  for (const sub of monorepoGlobs) {
187
128
  const subDir = join(projectDir, sub);
@@ -207,9 +148,9 @@ export function scanProject(projectDir) {
207
148
  };
208
149
 
209
150
  for (const [name, version] of Object.entries(allDeps)) {
210
- if (name in CRYPTO_PACKAGES && !seenPkgs.has(name)) {
151
+ const info = lookupNpmPackage(name);
152
+ if (info && !seenPkgs.has(name)) {
211
153
  seenPkgs.add(name);
212
- const info = CRYPTO_PACKAGES[name];
213
154
  results.libraries.push({
214
155
  name,
215
156
  version: version.replace(/^[\^~]/, ''),
@@ -217,37 +158,87 @@ export function scanProject(projectDir) {
217
158
  quantumRisk: info.quantumRisk,
218
159
  category: info.category,
219
160
  source: pkgPath.replace(projectDir + '/', ''),
161
+ ecosystem: 'npm',
220
162
  });
221
163
  }
222
164
  }
165
+ results.manifestsFound.push('package.json');
223
166
  } catch { /* invalid package.json */ }
224
167
  }
225
168
 
226
- // 2. Walk source files
227
- const files = walkFiles(projectDir);
169
+ // 2. Scan non-npm manifests (go.mod, requirements.txt, Cargo.toml, etc.)
170
+ const manifestLibs = scanManifests(projectDir);
171
+ for (const lib of manifestLibs) {
172
+ if (!seenPkgs.has(lib.name)) {
173
+ seenPkgs.add(lib.name);
174
+ results.libraries.push(lib);
175
+ if (!results.manifestsFound.includes(lib.source)) {
176
+ results.manifestsFound.push(lib.source);
177
+ }
178
+ }
179
+ }
180
+
181
+ // 3. Walk source files (single-pass unified walker with config overrides)
182
+ const scannerConfig = loadScannerConfig(projectDir);
183
+ const walked = walkProject(projectDir, {
184
+ skipDirs: scannerConfig.skipDirs.length > 0 ? new Set(scannerConfig.skipDirs) : undefined,
185
+ includeExtensions: scannerConfig.includeExtensions.length > 0 ? new Set(scannerConfig.includeExtensions) : undefined,
186
+ maxFiles: scannerConfig.maxFiles,
187
+ maxBytes: scannerConfig.maxBytes,
188
+ maxFileSize: scannerConfig.maxFileSize,
189
+ maxBinaryFiles: scannerConfig.binary.maxFiles,
190
+ maxBinaryFileSize: scannerConfig.binary.maxFileSize,
191
+ });
228
192
  const seenImports = new Set();
229
193
  const seenAlgos = new Set();
194
+ const seenSourceAlgos = new Set();
195
+
196
+ // Cert files from walker
197
+ for (const certPath of walked.certFiles) {
198
+ results.certFiles.push(relative(projectDir, certPath));
199
+ }
230
200
 
231
- for (const filePath of files) {
201
+ for (const filePath of walked.sourceFiles) {
232
202
  const ext = extname(filePath);
203
+ const relPath = relative(projectDir, filePath);
233
204
 
234
- // Check for cert/key files
235
- if (CERT_EXTENSIONS.has(ext)) {
236
- results.certFiles.push(relative(projectDir, filePath));
205
+ // Multi-language scanning (Go, Python, Java, Rust, C/C++)
206
+ const language = detectLanguage(filePath);
207
+ if (language && !JS_EXTENSIONS.has(ext)) {
208
+ results.filesScanned++;
209
+ results.languagesDetected.add(language);
210
+
211
+ let content;
212
+ try { content = readFileSync(filePath, 'utf-8'); }
213
+ catch { continue; }
214
+
215
+ const langResult = scanSourceFile(filePath, content, language);
216
+ for (const algo of langResult.algorithms) {
217
+ if (!seenSourceAlgos.has(algo.algorithm)) {
218
+ seenSourceAlgos.add(algo.algorithm);
219
+ const dbEntry = lookupAlgorithm(algo.algorithm);
220
+ results.sourceAlgorithms.push({
221
+ algorithm: algo.algorithm,
222
+ category: algo.category,
223
+ language,
224
+ quantumRisk: dbEntry?.quantumRisk || 'unknown',
225
+ isWeak: dbEntry?.isWeak || false,
226
+ });
227
+ }
228
+ }
237
229
  continue;
238
230
  }
239
231
 
240
- // Only scan source files for code patterns
241
- if (!SOURCE_EXTENSIONS.has(ext)) continue;
232
+ // Only scan JS/TS source files for JS-specific code patterns
233
+ if (!JS_EXTENSIONS.has(ext)) continue;
242
234
 
243
235
  results.filesScanned++;
236
+ results.languagesDetected.add('javascript');
244
237
 
245
238
  let content;
246
239
  try { content = readFileSync(filePath, 'utf-8'); }
247
240
  catch { continue; }
248
241
 
249
- const relPath = relative(projectDir, filePath);
250
-
251
242
  // Detect imports/requires
252
243
  for (const { pattern, lib, detail } of IMPORT_PATTERNS) {
253
244
  pattern.lastIndex = 0;
@@ -323,7 +314,6 @@ export function scanProject(projectDir) {
323
314
  }
324
315
 
325
316
  if (nodeCryptoAlgos.length > 0) {
326
- // Determine quantum risk based on detected algorithms
327
317
  const hasAsymmetric = nodeCryptoAlgos.some(a =>
328
318
  ['RSA', 'ECDSA', 'Ed25519', 'RS256', 'ES256', 'DH'].includes(a)
329
319
  );
@@ -334,10 +324,20 @@ export function scanProject(projectDir) {
334
324
  quantumRisk: hasAsymmetric ? 'high' : 'low',
335
325
  category: hasAsymmetric ? 'asymmetric' : 'symmetric',
336
326
  source: 'source-code',
327
+ ecosystem: 'npm',
337
328
  });
338
329
  }
339
330
  }
340
331
 
332
+ // 4. TLS scanning — pass pre-walked source+config files
333
+ const tlsFiles = [...walked.sourceFiles, ...walked.configFiles];
334
+ results.tlsFindings = scanTlsConfigs(projectDir, tlsFiles);
335
+
336
+ // 5. Binary scanning moved to CLI layer (lazy-loaded via dynamic import)
337
+
338
+ // Convert sets to arrays for JSON serialization
339
+ results.languagesDetected = [...results.languagesDetected];
340
+
341
341
  return results;
342
342
  }
343
343
 
@@ -346,12 +346,41 @@ export function scanProject(projectDir) {
346
346
  // ---------------------------------------------------------------------------
347
347
 
348
348
  export function toLibraryInventory(scanResults) {
349
- return scanResults.libraries.map(lib => ({
350
- name: lib.name,
351
- version: lib.version,
352
- algorithms: lib.algorithms,
353
- quantumRisk: lib.quantumRisk,
354
- category: lib.category,
355
- isDeprecated: false,
356
- }));
349
+ const inventory = scanResults.libraries.map(lib => {
350
+ const entry = {
351
+ name: lib.name,
352
+ version: lib.version,
353
+ algorithms: lib.algorithms,
354
+ quantumRisk: lib.quantumRisk,
355
+ category: lib.category,
356
+ isDeprecated: lib.isDeprecated || false,
357
+ };
358
+ // Enrich with algorithm-db data
359
+ for (const algoName of lib.algorithms) {
360
+ const dbEntry = lookupAlgorithm(algoName);
361
+ if (dbEntry?.isWeak && !entry.isDeprecated) {
362
+ entry.isDeprecated = true;
363
+ }
364
+ }
365
+ return entry;
366
+ });
367
+
368
+ // Also add source-detected algorithms as synthetic library entries
369
+ for (const algo of (scanResults.sourceAlgorithms || [])) {
370
+ const alreadyInLib = inventory.some(lib =>
371
+ lib.algorithms.some(a => a.toLowerCase() === algo.algorithm.toLowerCase())
372
+ );
373
+ if (!alreadyInLib) {
374
+ inventory.push({
375
+ name: `${algo.language}:${algo.algorithm}`,
376
+ version: 'source-code',
377
+ algorithms: [algo.algorithm],
378
+ quantumRisk: algo.quantumRisk || 'unknown',
379
+ category: algo.category,
380
+ isDeprecated: algo.isWeak || false,
381
+ });
382
+ }
383
+ }
384
+
385
+ return inventory;
357
386
  }
package/lib/walker.mjs ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Unified single-pass file walker.
3
+ *
4
+ * Replaces three independent walkers (scanner.mjs, scanner-tls.mjs,
5
+ * scanner-binary.mjs) with a single directory traversal that classifies
6
+ * files into buckets by extension.
7
+ * Zero dependencies — uses only node:fs and node:path.
8
+ */
9
+
10
+ import { readdirSync, statSync } from 'node:fs';
11
+ import { join, extname } from 'node:path';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Default skip directories (union of all three previous walkers)
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export const DEFAULT_SKIP_DIRS = new Set([
18
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
19
+ '.cache', '.nuxt', '.output', '.svelte-kit', '__pycache__',
20
+ 'vendor', '.venv', 'venv',
21
+ ]);
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // File classification by extension
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const SOURCE_EXTENSIONS = new Set([
28
+ '.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx',
29
+ '.go', '.py',
30
+ '.java', '.kt', '.scala',
31
+ '.rs',
32
+ '.c', '.h', '.cpp', '.hpp', '.cc', '.cxx',
33
+ ]);
34
+
35
+ const CONFIG_EXTENSIONS = new Set([
36
+ '.conf', '.cfg', '.ini', '.toml', '.yaml', '.yml', '.json', '.xml',
37
+ ]);
38
+
39
+ const CONFIG_NAMES = new Set([
40
+ 'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
41
+ 'nginx.conf', 'httpd.conf', 'apache2.conf', 'ssl.conf',
42
+ '.env',
43
+ ]);
44
+
45
+ const BINARY_EXTENSIONS = new Set([
46
+ '.exe', '.dll', '.so', '.dylib', '.wasm',
47
+ '.class', '.jar', '.war',
48
+ '.o', '.a', '.lib',
49
+ '.pyc', '.pyd',
50
+ ]);
51
+
52
+ const CERT_EXTENSIONS = new Set(['.pem', '.key', '.crt', '.p12', '.pfx', '.jks', '.keystore']);
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Walker
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Walk a project directory once, classifying files into buckets.
60
+ *
61
+ * @param {string} dir - Root directory to walk
62
+ * @param {object} options
63
+ * @param {Set<string>} [options.skipDirs] - Extra directory names to skip (merged with defaults)
64
+ * @param {Set<string>} [options.includeExtensions] - Extra source extensions to include
65
+ * @param {number} [options.maxFiles=10000] - Max total files to collect
66
+ * @param {number} [options.maxBytes=524288000] - Max total bytes (500MB)
67
+ * @param {number} [options.maxFileSize=1048576] - Max single file size (1MB) for source/config
68
+ * @param {number} [options.maxBinaryFiles=50] - Max binary files
69
+ * @param {number} [options.maxBinaryFileSize=10485760] - Max binary file size (10MB)
70
+ * @returns {{ sourceFiles: string[], configFiles: string[], binaryFiles: string[], certFiles: string[], totalBytes: number, totalFiles: number }}
71
+ */
72
+ export function walkProject(dir, options = {}) {
73
+ const skipDirs = options.skipDirs
74
+ ? new Set([...DEFAULT_SKIP_DIRS, ...options.skipDirs])
75
+ : DEFAULT_SKIP_DIRS;
76
+ const extraSourceExts = options.includeExtensions
77
+ ? new Set([...SOURCE_EXTENSIONS, ...options.includeExtensions])
78
+ : SOURCE_EXTENSIONS;
79
+ const maxFiles = options.maxFiles || 10000;
80
+ const maxBytes = options.maxBytes || 500 * 1024 * 1024;
81
+ const maxFileSize = options.maxFileSize || 1024 * 1024;
82
+ const maxBinaryFiles = options.maxBinaryFiles || 50;
83
+ const maxBinaryFileSize = options.maxBinaryFileSize || 10 * 1024 * 1024;
84
+
85
+ const sourceFiles = [];
86
+ const configFiles = [];
87
+ const binaryFiles = [];
88
+ const certFiles = [];
89
+ let totalBytes = 0;
90
+ let totalFiles = 0;
91
+
92
+ function walk(currentDir) {
93
+ if (totalFiles >= maxFiles || totalBytes >= maxBytes) return;
94
+
95
+ let entries;
96
+ try { entries = readdirSync(currentDir, { withFileTypes: true }); }
97
+ catch { return; }
98
+
99
+ for (const entry of entries) {
100
+ if (totalFiles >= maxFiles || totalBytes >= maxBytes) return;
101
+
102
+ if (entry.isDirectory()) {
103
+ if (!skipDirs.has(entry.name) && !entry.name.startsWith('.')) {
104
+ walk(join(currentDir, entry.name));
105
+ }
106
+ continue;
107
+ }
108
+
109
+ if (!entry.isFile()) continue;
110
+
111
+ const filePath = join(currentDir, entry.name);
112
+ const ext = extname(entry.name).toLowerCase();
113
+ const name = entry.name;
114
+
115
+ // Classify cert files (no size check needed — just record path)
116
+ if (CERT_EXTENSIONS.has(ext)) {
117
+ certFiles.push(filePath);
118
+ totalFiles++;
119
+ continue;
120
+ }
121
+
122
+ // Classify binary files (separate limit)
123
+ if (BINARY_EXTENSIONS.has(ext)) {
124
+ if (binaryFiles.length < maxBinaryFiles) {
125
+ try {
126
+ const stat = statSync(filePath);
127
+ if (stat.size <= maxBinaryFileSize) {
128
+ binaryFiles.push(filePath);
129
+ totalFiles++;
130
+ }
131
+ } catch { /* skip */ }
132
+ }
133
+ continue;
134
+ }
135
+
136
+ // Check size for source/config files
137
+ let fileSize;
138
+ try {
139
+ const stat = statSync(filePath);
140
+ if (stat.size > maxFileSize) continue;
141
+ fileSize = stat.size;
142
+ } catch { continue; }
143
+
144
+ totalBytes += fileSize;
145
+ totalFiles++;
146
+
147
+ // Classify source files
148
+ if (extraSourceExts.has(ext)) {
149
+ sourceFiles.push(filePath);
150
+ continue;
151
+ }
152
+
153
+ // Classify config files
154
+ if (CONFIG_EXTENSIONS.has(ext) || CONFIG_NAMES.has(name)) {
155
+ configFiles.push(filePath);
156
+ continue;
157
+ }
158
+
159
+ // Other files are walked but not bucketed
160
+ }
161
+ }
162
+
163
+ walk(dir);
164
+
165
+ return { sourceFiles, configFiles, binaryFiles, certFiles, totalBytes, totalFiles };
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cryptoserve",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "CryptoServe CLI - Cryptographic scanning, PQC analysis, encryption, and local key management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,12 @@
24
24
  "security",
25
25
  "scanner",
26
26
  "keychain",
27
- "vault"
27
+ "vault",
28
+ "cbom",
29
+ "cyclonedx",
30
+ "spdx",
31
+ "binary-analysis",
32
+ "tls"
28
33
  ],
29
34
  "repository": {
30
35
  "type": "git",