@cyvest/cyvest-js 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/src/keys.ts ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Key generation utilities for Cyvest objects.
3
+ *
4
+ * Provides deterministic, unique key generation for all object types.
5
+ * Keys are used for object identification, retrieval, and merging.
6
+ */
7
+
8
+ /**
9
+ * Key type prefixes used in Cyvest.
10
+ */
11
+ export type KeyType = "obs" | "chk" | "ti" | "enr" | "ctr";
12
+
13
+ /**
14
+ * Normalize a string value for consistent key generation.
15
+ */
16
+ function normalizeValue(value: string): string {
17
+ return value.trim().toLowerCase();
18
+ }
19
+
20
+ /**
21
+ * Create a deterministic hash from a string using a simple hash algorithm.
22
+ * Uses a subset of characters for shorter keys.
23
+ */
24
+ function hashString(content: string, length: number = 16): string {
25
+ // Simple hash implementation (similar to Java's hashCode)
26
+ // For production, consider using crypto.subtle or a library
27
+ let hash = 0;
28
+ for (let i = 0; i < content.length; i++) {
29
+ const char = content.charCodeAt(i);
30
+ hash = ((hash << 5) - hash + char) | 0;
31
+ }
32
+ // Convert to hex and pad/truncate
33
+ const hex = Math.abs(hash).toString(16).padStart(8, "0");
34
+ return hex.slice(0, length);
35
+ }
36
+
37
+ /**
38
+ * Generate a unique key for an observable.
39
+ *
40
+ * Format: obs:{type}:{normalized_value}
41
+ *
42
+ * @param obsType - Type of observable (ip, url, domain, hash, etc.)
43
+ * @param value - Value of the observable
44
+ * @returns Unique observable key
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * generateObservableKey("ipv4-addr", "192.168.1.1")
49
+ * // => "obs:ipv4-addr:192.168.1.1"
50
+ * ```
51
+ */
52
+ export function generateObservableKey(obsType: string, value: string): string {
53
+ const normalizedType = normalizeValue(obsType);
54
+ const normalizedValue = normalizeValue(value);
55
+ return `obs:${normalizedType}:${normalizedValue}`;
56
+ }
57
+
58
+ /**
59
+ * Generate a unique key for a check.
60
+ *
61
+ * Format: chk:{check_id}:{scope}
62
+ *
63
+ * @param checkId - Identifier of the check
64
+ * @param scope - Scope of the check
65
+ * @returns Unique check key
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * generateCheckKey("sender_verification", "email_headers")
70
+ * // => "chk:sender_verification:email_headers"
71
+ * ```
72
+ */
73
+ export function generateCheckKey(checkId: string, scope: string): string {
74
+ const normalizedId = normalizeValue(checkId);
75
+ const normalizedScope = normalizeValue(scope);
76
+ return `chk:${normalizedId}:${normalizedScope}`;
77
+ }
78
+
79
+ /**
80
+ * Generate a unique key for threat intelligence.
81
+ *
82
+ * Format: ti:{normalized_source}:{observable_key}
83
+ *
84
+ * @param source - Name of the threat intel source
85
+ * @param observableKey - Key of the related observable
86
+ * @returns Unique threat intel key
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * generateThreatIntelKey("virustotal", "obs:ipv4-addr:192.168.1.1")
91
+ * // => "ti:virustotal:obs:ipv4-addr:192.168.1.1"
92
+ * ```
93
+ */
94
+ export function generateThreatIntelKey(
95
+ source: string,
96
+ observableKey: string
97
+ ): string {
98
+ const normalizedSource = normalizeValue(source);
99
+ return `ti:${normalizedSource}:${observableKey}`;
100
+ }
101
+
102
+ /**
103
+ * Generate a unique key for an enrichment.
104
+ *
105
+ * Format: enr:{name} or enr:{name}:{context_hash}
106
+ *
107
+ * @param name - Name of the enrichment
108
+ * @param context - Optional context string for disambiguation
109
+ * @returns Unique enrichment key
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * generateEnrichmentKey("whois_data")
114
+ * // => "enr:whois_data"
115
+ *
116
+ * generateEnrichmentKey("whois_data", "domain:example.com")
117
+ * // => "enr:whois_data:a1b2c3d4"
118
+ * ```
119
+ */
120
+ export function generateEnrichmentKey(name: string, context?: string): string {
121
+ const normalizedName = normalizeValue(name);
122
+ if (context) {
123
+ const contextHash = hashString(context, 8);
124
+ return `enr:${normalizedName}:${contextHash}`;
125
+ }
126
+ return `enr:${normalizedName}`;
127
+ }
128
+
129
+ /**
130
+ * Generate a unique key for a container.
131
+ *
132
+ * Format: ctr:{normalized_path}
133
+ *
134
+ * @param path - Path of the container (can use / or . as separator)
135
+ * @returns Unique container key
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * generateContainerKey("email/headers")
140
+ * // => "ctr:email/headers"
141
+ * ```
142
+ */
143
+ export function generateContainerKey(path: string): string {
144
+ // Normalize path separators
145
+ let normalizedPath = path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
146
+ normalizedPath = normalizeValue(normalizedPath);
147
+ return `ctr:${normalizedPath}`;
148
+ }
149
+
150
+ /**
151
+ * Extract the type prefix from a key.
152
+ *
153
+ * @param key - The key to parse
154
+ * @returns Type prefix (obs, chk, ti, enr, ctr) or null if invalid
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * parseKeyType("obs:ipv4-addr:192.168.1.1") // => "obs"
159
+ * parseKeyType("invalid") // => null
160
+ * ```
161
+ */
162
+ export function parseKeyType(key: string): KeyType | null {
163
+ if (key.includes(":")) {
164
+ const prefix = key.split(":", 1)[0] as KeyType;
165
+ if (["obs", "chk", "ti", "enr", "ctr"].includes(prefix)) {
166
+ return prefix;
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Validate a key format and optionally check its type.
174
+ *
175
+ * @param key - The key to validate
176
+ * @param expectedType - Optional expected type prefix
177
+ * @returns True if valid, false otherwise
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * validateKey("obs:ipv4-addr:192.168.1.1") // => true
182
+ * validateKey("obs:ipv4-addr:192.168.1.1", "obs") // => true
183
+ * validateKey("obs:ipv4-addr:192.168.1.1", "chk") // => false
184
+ * validateKey("invalid") // => false
185
+ * ```
186
+ */
187
+ export function validateKey(key: string, expectedType?: KeyType): boolean {
188
+ if (!key || !key.includes(":")) {
189
+ return false;
190
+ }
191
+
192
+ const keyType = parseKeyType(key);
193
+ if (!keyType) {
194
+ return false;
195
+ }
196
+
197
+ if (expectedType && keyType !== expectedType) {
198
+ return false;
199
+ }
200
+
201
+ return true;
202
+ }
203
+
204
+ /**
205
+ * Extract components from an observable key.
206
+ *
207
+ * @param key - Observable key to parse
208
+ * @returns Object with type and value, or null if invalid
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * parseObservableKey("obs:ipv4-addr:192.168.1.1")
213
+ * // => { type: "ipv4-addr", value: "192.168.1.1" }
214
+ * ```
215
+ */
216
+ export function parseObservableKey(
217
+ key: string
218
+ ): { type: string; value: string } | null {
219
+ if (!validateKey(key, "obs")) {
220
+ return null;
221
+ }
222
+ const parts = key.split(":");
223
+ if (parts.length >= 3) {
224
+ return {
225
+ type: parts[1],
226
+ value: parts.slice(2).join(":"), // Handle values with colons
227
+ };
228
+ }
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ * Extract components from a check key.
234
+ *
235
+ * @param key - Check key to parse
236
+ * @returns Object with checkId and scope, or null if invalid
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * parseCheckKey("chk:sender_verification:email_headers")
241
+ * // => { checkId: "sender_verification", scope: "email_headers" }
242
+ * ```
243
+ */
244
+ export function parseCheckKey(
245
+ key: string
246
+ ): { checkId: string; scope: string } | null {
247
+ if (!validateKey(key, "chk")) {
248
+ return null;
249
+ }
250
+ const parts = key.split(":");
251
+ if (parts.length >= 3) {
252
+ return {
253
+ checkId: parts[1],
254
+ scope: parts.slice(2).join(":"),
255
+ };
256
+ }
257
+ return null;
258
+ }
259
+
260
+ /**
261
+ * Extract components from a threat intel key.
262
+ *
263
+ * @param key - Threat intel key to parse
264
+ * @returns Object with source and observableKey, or null if invalid
265
+ *
266
+ * @example
267
+ * ```ts
268
+ * parseThreatIntelKey("ti:virustotal:obs:ipv4-addr:192.168.1.1")
269
+ * // => { source: "virustotal", observableKey: "obs:ipv4-addr:192.168.1.1" }
270
+ * ```
271
+ */
272
+ export function parseThreatIntelKey(
273
+ key: string
274
+ ): { source: string; observableKey: string } | null {
275
+ if (!validateKey(key, "ti")) {
276
+ return null;
277
+ }
278
+ const parts = key.split(":");
279
+ if (parts.length >= 3) {
280
+ return {
281
+ source: parts[1],
282
+ observableKey: parts.slice(2).join(":"),
283
+ };
284
+ }
285
+ return null;
286
+ }
package/src/levels.ts ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Level enumeration and scoring logic for Cyvest.
3
+ *
4
+ * This module defines the security level classification system and the algorithm
5
+ * for determining levels from scores.
6
+ */
7
+
8
+ import type { Observable, Check, ThreatIntel, Container, Level } from "./types.generated";
9
+
10
+ // Re-export Level type from types.generated for convenience
11
+ export type { Level } from "./types.generated";
12
+
13
+ /**
14
+ * Ordered array of levels from lowest to highest severity.
15
+ */
16
+ export const LEVEL_ORDER: readonly Level[] = [
17
+ "NONE",
18
+ "TRUSTED",
19
+ "INFO",
20
+ "SAFE",
21
+ "NOTABLE",
22
+ "SUSPICIOUS",
23
+ "MALICIOUS",
24
+ ] as const;
25
+
26
+ /**
27
+ * Numeric values for each level (for comparison purposes).
28
+ */
29
+ export const LEVEL_VALUES: Record<Level, number> = {
30
+ NONE: 0,
31
+ TRUSTED: 1,
32
+ INFO: 2,
33
+ SAFE: 3,
34
+ NOTABLE: 4,
35
+ SUSPICIOUS: 5,
36
+ MALICIOUS: 6,
37
+ };
38
+
39
+ /**
40
+ * Color mapping for display purposes.
41
+ */
42
+ export const LEVEL_COLORS: Record<Level, string> = {
43
+ NONE: "#808080", // gray
44
+ TRUSTED: "#22c55e", // green
45
+ INFO: "#06b6d4", // cyan
46
+ SAFE: "#4ade80", // bright green
47
+ NOTABLE: "#eab308", // yellow
48
+ SUSPICIOUS: "#f97316", // orange
49
+ MALICIOUS: "#ef4444", // red
50
+ };
51
+
52
+ /**
53
+ * Normalize a level input to the Level type.
54
+ *
55
+ * Accepts a case-insensitive string and returns the normalized Level.
56
+ *
57
+ * @param level - Level string (e.g., "malicious", "MALICIOUS")
58
+ * @returns The normalized Level
59
+ * @throws Error if the string does not match a valid Level
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * normalizeLevel("malicious") // => "MALICIOUS"
64
+ * normalizeLevel("TRUSTED") // => "TRUSTED"
65
+ * ```
66
+ */
67
+ export function normalizeLevel(level: string): Level {
68
+ const upper = level.toUpperCase();
69
+ if (LEVEL_ORDER.includes(upper as Level)) {
70
+ return upper as Level;
71
+ }
72
+ throw new Error(`Invalid level name: ${level}`);
73
+ }
74
+
75
+ /**
76
+ * Check if a string is a valid Level.
77
+ *
78
+ * @param level - String to check
79
+ * @returns True if valid Level
80
+ */
81
+ export function isValidLevel(level: string): level is Level {
82
+ return LEVEL_ORDER.includes(level.toUpperCase() as Level);
83
+ }
84
+
85
+ /**
86
+ * Calculate the security level from a numeric score.
87
+ *
88
+ * Algorithm:
89
+ * - score < 0.0 -> TRUSTED
90
+ * - score === 0.0 -> INFO
91
+ * - score < 3.0 -> NOTABLE
92
+ * - score < 5.0 -> SUSPICIOUS
93
+ * - score >= 5.0 -> MALICIOUS
94
+ *
95
+ * @param score - The numeric score to evaluate
96
+ * @returns The appropriate Level based on the score
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * getLevelFromScore(-1) // => "TRUSTED"
101
+ * getLevelFromScore(0) // => "INFO"
102
+ * getLevelFromScore(2.5) // => "NOTABLE"
103
+ * getLevelFromScore(4) // => "SUSPICIOUS"
104
+ * getLevelFromScore(5) // => "MALICIOUS"
105
+ * ```
106
+ */
107
+ export function getLevelFromScore(score: number): Level {
108
+ if (score < 0) {
109
+ return "TRUSTED";
110
+ }
111
+ if (score === 0) {
112
+ return "INFO";
113
+ }
114
+ if (score < 3) {
115
+ return "NOTABLE";
116
+ }
117
+ if (score < 5) {
118
+ return "SUSPICIOUS";
119
+ }
120
+ return "MALICIOUS";
121
+ }
122
+
123
+ /**
124
+ * Compare two levels.
125
+ *
126
+ * @param a - First level
127
+ * @param b - Second level
128
+ * @returns -1 if a < b, 0 if a === b, 1 if a > b
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * compareLevels("INFO", "MALICIOUS") // => -1
133
+ * compareLevels("MALICIOUS", "INFO") // => 1
134
+ * compareLevels("INFO", "INFO") // => 0
135
+ * ```
136
+ */
137
+ export function compareLevels(a: Level, b: Level): -1 | 0 | 1 {
138
+ const valueA = LEVEL_VALUES[a];
139
+ const valueB = LEVEL_VALUES[b];
140
+ if (valueA < valueB) return -1;
141
+ if (valueA > valueB) return 1;
142
+ return 0;
143
+ }
144
+
145
+ /**
146
+ * Check if level a is higher (more severe) than level b.
147
+ *
148
+ * @param a - First level
149
+ * @param b - Second level
150
+ * @returns True if a is higher than b
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * isLevelHigherThan("MALICIOUS", "SUSPICIOUS") // => true
155
+ * isLevelHigherThan("INFO", "MALICIOUS") // => false
156
+ * ```
157
+ */
158
+ export function isLevelHigherThan(a: Level, b: Level): boolean {
159
+ return LEVEL_VALUES[a] > LEVEL_VALUES[b];
160
+ }
161
+
162
+ /**
163
+ * Check if level a is lower (less severe) than level b.
164
+ *
165
+ * @param a - First level
166
+ * @param b - Second level
167
+ * @returns True if a is lower than b
168
+ */
169
+ export function isLevelLowerThan(a: Level, b: Level): boolean {
170
+ return LEVEL_VALUES[a] < LEVEL_VALUES[b];
171
+ }
172
+
173
+ /**
174
+ * Check if level a is at least as severe as level b.
175
+ *
176
+ * @param a - Level to check
177
+ * @param minLevel - Minimum required level
178
+ * @returns True if a is at least minLevel
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * isLevelAtLeast("MALICIOUS", "SUSPICIOUS") // => true
183
+ * isLevelAtLeast("SUSPICIOUS", "SUSPICIOUS") // => true
184
+ * isLevelAtLeast("INFO", "SUSPICIOUS") // => false
185
+ * ```
186
+ */
187
+ export function isLevelAtLeast(a: Level, minLevel: Level): boolean {
188
+ return LEVEL_VALUES[a] >= LEVEL_VALUES[minLevel];
189
+ }
190
+
191
+ /**
192
+ * Get the maximum (most severe) level from an array of levels.
193
+ *
194
+ * @param levels - Array of levels
195
+ * @returns The most severe level, or "NONE" if array is empty
196
+ */
197
+ export function maxLevel(levels: Level[]): Level {
198
+ if (levels.length === 0) return "NONE";
199
+ return levels.reduce((max, level) =>
200
+ isLevelHigherThan(level, max) ? level : max
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Get the minimum (least severe) level from an array of levels.
206
+ *
207
+ * @param levels - Array of levels
208
+ * @returns The least severe level, or "MALICIOUS" if array is empty
209
+ */
210
+ export function minLevel(levels: Level[]): Level {
211
+ if (levels.length === 0) return "MALICIOUS";
212
+ return levels.reduce((min, level) =>
213
+ isLevelLowerThan(level, min) ? level : min
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Get the color associated with a level for display purposes.
219
+ *
220
+ * @param level - Level to get color for
221
+ * @returns Hex color string
222
+ */
223
+ export function getColorForLevel(level: Level): string {
224
+ return LEVEL_COLORS[level];
225
+ }
226
+
227
+ /**
228
+ * Get the color associated with a score for display purposes.
229
+ *
230
+ * @param score - Score to get color for
231
+ * @returns Hex color string
232
+ */
233
+ export function getColorForScore(score: number): string {
234
+ return getColorForLevel(getLevelFromScore(score));
235
+ }
236
+
237
+ /**
238
+ * Type guard to check if an object has a level property.
239
+ */
240
+ export function hasLevel(
241
+ obj: unknown
242
+ ): obj is { level: Level } {
243
+ return (
244
+ typeof obj === "object" &&
245
+ obj !== null &&
246
+ "level" in obj &&
247
+ typeof (obj as { level: unknown }).level === "string" &&
248
+ isValidLevel((obj as { level: string }).level)
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Extract level from an entity (Observable, Check, ThreatIntel, Container).
254
+ */
255
+ export function getEntityLevel(
256
+ entity: Observable | Check | ThreatIntel | Container
257
+ ): Level {
258
+ if ("aggregated_level" in entity) {
259
+ return entity.aggregated_level;
260
+ }
261
+ return entity.level;
262
+ }
@@ -0,0 +1,176 @@
1
+ // AUTO-GENERATED FROM cyvest.schema.json — DO NOT EDIT
2
+
3
+ /**
4
+ * Security level classification from NONE (lowest) to MALICIOUS (highest).
5
+ */
6
+ export type Level =
7
+ | "NONE"
8
+ | "TRUSTED"
9
+ | "INFO"
10
+ | "SAFE"
11
+ | "NOTABLE"
12
+ | "SUSPICIOUS"
13
+ | "MALICIOUS";
14
+ /**
15
+ * Direction of a relationship between observables.
16
+ */
17
+ export type RelationshipDirection = "outbound" | "inbound" | "bidirectional";
18
+ /**
19
+ * Score computation policy: 'auto' calculates from level, 'manual' uses explicit score.
20
+ */
21
+ export type ScorePolicy = "auto" | "manual";
22
+ /**
23
+ * Score aggregation mode: 'max' takes highest score, 'sum' adds all scores.
24
+ */
25
+ export type ScoreMode = "max" | "sum";
26
+
27
+ export interface CyvestInvestigation {
28
+ score: number;
29
+ level: Level;
30
+ whitelisted: boolean;
31
+ whitelists: Whitelist[];
32
+ observables: {
33
+ [k: string]: Observable;
34
+ };
35
+ checks: {
36
+ [k: string]: Check[];
37
+ };
38
+ checks_by_level: {
39
+ [k: string]: string[];
40
+ };
41
+ threat_intels: {
42
+ [k: string]: ThreatIntel;
43
+ };
44
+ enrichments: {
45
+ [k: string]: Enrichment;
46
+ };
47
+ containers: {
48
+ [k: string]: Container;
49
+ };
50
+ stats: Statistics;
51
+ stats_checks: StatsChecks;
52
+ data_extraction: DataExtraction;
53
+ }
54
+ export interface Whitelist {
55
+ identifier: string;
56
+ name: string;
57
+ justification?: string | null;
58
+ }
59
+ export interface Observable {
60
+ key: string;
61
+ /**
62
+ * Observable type (e.g., ipv4-addr, url). Custom values are allowed.
63
+ */
64
+ type: string;
65
+ value: string;
66
+ internal: boolean;
67
+ whitelisted: boolean;
68
+ comment: string;
69
+ extra: {
70
+ [k: string]: unknown;
71
+ } | null;
72
+ score: number;
73
+ level: Level;
74
+ relationships: Relationship[];
75
+ threat_intels: string[];
76
+ generated_by_checks: string[];
77
+ }
78
+ export interface Relationship {
79
+ target_key: string;
80
+ /**
81
+ * Relationship label; defaults to related-to.
82
+ */
83
+ relationship_type: string;
84
+ direction: RelationshipDirection;
85
+ }
86
+ export interface Check {
87
+ key: string;
88
+ check_id: string;
89
+ scope: string;
90
+ description: string;
91
+ comment: string;
92
+ extra: {
93
+ [k: string]: unknown;
94
+ } | null;
95
+ score: number;
96
+ level: Level;
97
+ score_policy: ScorePolicy;
98
+ observables: string[];
99
+ }
100
+ export interface ThreatIntel {
101
+ key: string;
102
+ source: string;
103
+ observable_key: string;
104
+ comment: string;
105
+ extra: {
106
+ [k: string]: unknown;
107
+ } | null;
108
+ score: number;
109
+ level: Level;
110
+ taxonomies: {
111
+ [k: string]: unknown;
112
+ }[];
113
+ }
114
+ export interface Enrichment {
115
+ key: string;
116
+ name: string;
117
+ data: {
118
+ [k: string]: unknown;
119
+ };
120
+ context: string;
121
+ }
122
+ export interface Container {
123
+ key: string;
124
+ path: string;
125
+ description: string;
126
+ checks: string[];
127
+ sub_containers: {
128
+ [k: string]: Container;
129
+ };
130
+ aggregated_score: number;
131
+ aggregated_level: Level;
132
+ }
133
+ export interface Statistics {
134
+ total_observables: number;
135
+ internal_observables: number;
136
+ external_observables: number;
137
+ whitelisted_observables: number;
138
+ observables_by_type: {
139
+ [k: string]: number;
140
+ };
141
+ observables_by_level: {
142
+ [k: string]: number;
143
+ };
144
+ observables_by_type_and_level: {
145
+ [k: string]: {
146
+ [k: string]: number;
147
+ };
148
+ };
149
+ total_checks: number;
150
+ applied_checks: number;
151
+ checks_by_scope: {
152
+ [k: string]: number;
153
+ };
154
+ checks_by_level: {
155
+ [k: string]: number;
156
+ };
157
+ total_threat_intel: number;
158
+ threat_intel_by_source: {
159
+ [k: string]: number;
160
+ };
161
+ threat_intel_by_level: {
162
+ [k: string]: number;
163
+ };
164
+ total_containers: number;
165
+ }
166
+ export interface StatsChecks {
167
+ checks: number;
168
+ applied: number;
169
+ }
170
+ export interface DataExtraction {
171
+ /**
172
+ * Root observable type used during data extraction.
173
+ */
174
+ root_type: string | null;
175
+ score_mode: ScoreMode;
176
+ }