@sentriflow/core 0.3.0 → 0.4.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/package.json +65 -65
- package/src/grx2-loader/GRX2ExtendedLoader.ts +2 -3
- package/src/grx2-loader/index.ts +2 -0
- package/src/grx2-loader/types.ts +13 -6
- package/src/helpers/cisco/helpers.ts +19 -0
- package/src/helpers/cisco/index.ts +1 -0
- package/src/helpers/common/helpers.ts +29 -0
- package/src/ip/classifier.ts +416 -0
- package/src/ip/index.ts +16 -0
- package/src/json-rules/schema.json +1 -1
- package/src/pack-loader/index.ts +1 -1
- package/src/pack-provider/PackProvider.ts +1 -1
package/package.json
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sentriflow/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "SentriFlow core engine for network configuration validation",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"module": "src/index.ts",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./src/index.ts",
|
|
10
|
-
"./grx2-loader": "./src/grx2-loader/index.ts",
|
|
11
|
-
"./helpers": "./src/helpers/index.ts",
|
|
12
|
-
"./helpers/common": "./src/helpers/common/index.ts",
|
|
13
|
-
"./helpers/arista": "./src/helpers/arista/index.ts",
|
|
14
|
-
"./helpers/aruba": "./src/helpers/aruba/index.ts",
|
|
15
|
-
"./helpers/cisco": "./src/helpers/cisco/index.ts",
|
|
16
|
-
"./helpers/cumulus": "./src/helpers/cumulus/index.ts",
|
|
17
|
-
"./helpers/extreme": "./src/helpers/extreme/index.ts",
|
|
18
|
-
"./helpers/fortinet": "./src/helpers/fortinet/index.ts",
|
|
19
|
-
"./helpers/huawei": "./src/helpers/huawei/index.ts",
|
|
20
|
-
"./helpers/juniper": "./src/helpers/juniper/index.ts",
|
|
21
|
-
"./helpers/mikrotik": "./src/helpers/mikrotik/index.ts",
|
|
22
|
-
"./helpers/nokia": "./src/helpers/nokia/index.ts",
|
|
23
|
-
"./helpers/paloalto": "./src/helpers/paloalto/index.ts",
|
|
24
|
-
"./helpers/vyos": "./src/helpers/vyos/index.ts"
|
|
25
|
-
},
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/sentriflow/sentriflow.git",
|
|
29
|
-
"directory": "packages/core"
|
|
30
|
-
},
|
|
31
|
-
"homepage": "https://github.com/sentriflow/sentriflow#readme",
|
|
32
|
-
"bugs": {
|
|
33
|
-
"url": "https://github.com/sentriflow/sentriflow/issues"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"network",
|
|
37
|
-
"configuration",
|
|
38
|
-
"validation",
|
|
39
|
-
"security",
|
|
40
|
-
"cisco",
|
|
41
|
-
"juniper",
|
|
42
|
-
"arista",
|
|
43
|
-
"firewall",
|
|
44
|
-
"linter",
|
|
45
|
-
"helpers"
|
|
46
|
-
],
|
|
47
|
-
"files": [
|
|
48
|
-
"src",
|
|
49
|
-
"LICENSE",
|
|
50
|
-
"README.md"
|
|
51
|
-
],
|
|
52
|
-
"publishConfig": {
|
|
53
|
-
"access": "public"
|
|
54
|
-
},
|
|
55
|
-
"dependencies": {
|
|
56
|
-
"node-machine-id": "^1.1.12"
|
|
57
|
-
},
|
|
58
|
-
"devDependencies": {
|
|
59
|
-
"bun-types": "latest",
|
|
60
|
-
"@types/node": "^20.0.0"
|
|
61
|
-
},
|
|
62
|
-
"peerDependencies": {
|
|
63
|
-
"typescript": "^5.0.0"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@sentriflow/core",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "SentriFlow core engine for network configuration validation",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"module": "src/index.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./grx2-loader": "./src/grx2-loader/index.ts",
|
|
11
|
+
"./helpers": "./src/helpers/index.ts",
|
|
12
|
+
"./helpers/common": "./src/helpers/common/index.ts",
|
|
13
|
+
"./helpers/arista": "./src/helpers/arista/index.ts",
|
|
14
|
+
"./helpers/aruba": "./src/helpers/aruba/index.ts",
|
|
15
|
+
"./helpers/cisco": "./src/helpers/cisco/index.ts",
|
|
16
|
+
"./helpers/cumulus": "./src/helpers/cumulus/index.ts",
|
|
17
|
+
"./helpers/extreme": "./src/helpers/extreme/index.ts",
|
|
18
|
+
"./helpers/fortinet": "./src/helpers/fortinet/index.ts",
|
|
19
|
+
"./helpers/huawei": "./src/helpers/huawei/index.ts",
|
|
20
|
+
"./helpers/juniper": "./src/helpers/juniper/index.ts",
|
|
21
|
+
"./helpers/mikrotik": "./src/helpers/mikrotik/index.ts",
|
|
22
|
+
"./helpers/nokia": "./src/helpers/nokia/index.ts",
|
|
23
|
+
"./helpers/paloalto": "./src/helpers/paloalto/index.ts",
|
|
24
|
+
"./helpers/vyos": "./src/helpers/vyos/index.ts"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/sentriflow/sentriflow.git",
|
|
29
|
+
"directory": "packages/core"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/sentriflow/sentriflow#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/sentriflow/sentriflow/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"network",
|
|
37
|
+
"configuration",
|
|
38
|
+
"validation",
|
|
39
|
+
"security",
|
|
40
|
+
"cisco",
|
|
41
|
+
"juniper",
|
|
42
|
+
"arista",
|
|
43
|
+
"firewall",
|
|
44
|
+
"linter",
|
|
45
|
+
"helpers"
|
|
46
|
+
],
|
|
47
|
+
"files": [
|
|
48
|
+
"src",
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"README.md"
|
|
51
|
+
],
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"node-machine-id": "^1.1.12"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"bun-types": "latest",
|
|
60
|
+
"@types/node": "^20.0.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"typescript": "^5.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -358,9 +358,8 @@ export async function loadExtendedPack(
|
|
|
358
358
|
machineId: string,
|
|
359
359
|
debug?: (msg: string) => void
|
|
360
360
|
): Promise<RulePack> {
|
|
361
|
-
debug?.(`[GRX2Loader] Loading pack: ${filePath}`);
|
|
362
|
-
debug?.(`[GRX2Loader]
|
|
363
|
-
debug?.(`[GRX2Loader] Machine ID: "${machineId}" (length: ${machineId.length})`);
|
|
361
|
+
debug?.(`[GRX2Loader] Loading pack: ${filePath}`);
|
|
362
|
+
debug?.(`[GRX2Loader] Machine ID: "${machineId}" (length: ${machineId.length})`);
|
|
364
363
|
|
|
365
364
|
// Read pack file
|
|
366
365
|
const data = await readFile(filePath);
|
package/src/grx2-loader/index.ts
CHANGED
package/src/grx2-loader/types.ts
CHANGED
|
@@ -25,13 +25,13 @@ export interface LicensePayload {
|
|
|
25
25
|
sub: string;
|
|
26
26
|
|
|
27
27
|
/** Customer tier */
|
|
28
|
-
tier: '
|
|
28
|
+
tier: 'basic' | 'professional' | 'enterprise';
|
|
29
29
|
|
|
30
30
|
/** Entitled feed IDs */
|
|
31
31
|
feeds: string[];
|
|
32
32
|
|
|
33
|
-
/** API URL for cloud updates */
|
|
34
|
-
api
|
|
33
|
+
/** API URL for cloud updates (optional - derived from domain at runtime if not set) */
|
|
34
|
+
api?: string;
|
|
35
35
|
|
|
36
36
|
/** Expiration timestamp (Unix seconds) */
|
|
37
37
|
exp: number;
|
|
@@ -229,7 +229,8 @@ export type EncryptedPackErrorCode =
|
|
|
229
229
|
| 'DECRYPTION_FAILED'
|
|
230
230
|
| 'MACHINE_MISMATCH'
|
|
231
231
|
| 'NETWORK_ERROR'
|
|
232
|
-
| 'API_ERROR'
|
|
232
|
+
| 'API_ERROR'
|
|
233
|
+
| 'ACTIVATION_FAILED';
|
|
233
234
|
|
|
234
235
|
/**
|
|
235
236
|
* Encrypted pack error
|
|
@@ -273,8 +274,14 @@ export const GRX2_KEY_TYPE_TMK = 1;
|
|
|
273
274
|
/** CTMK key type */
|
|
274
275
|
export const GRX2_KEY_TYPE_CTMK = 2;
|
|
275
276
|
|
|
277
|
+
/** Base SentriFlow home directory (platform-aware: ~/.sentriflow or %USERPROFILE%\.sentriflow) */
|
|
278
|
+
export const SENTRIFLOW_HOME = join(homedir(), '.sentriflow');
|
|
279
|
+
|
|
276
280
|
/** Default packs directory (platform-aware) */
|
|
277
|
-
export const DEFAULT_PACKS_DIRECTORY = join(
|
|
281
|
+
export const DEFAULT_PACKS_DIRECTORY = join(SENTRIFLOW_HOME, 'packs');
|
|
282
|
+
|
|
283
|
+
/** Default custom rules directory (platform-aware) */
|
|
284
|
+
export const DEFAULT_RULES_DIRECTORY = join(SENTRIFLOW_HOME, 'rules');
|
|
278
285
|
|
|
279
286
|
/** Cache directory (for downloaded packs, platform-aware) */
|
|
280
|
-
export const CACHE_DIRECTORY = join(
|
|
287
|
+
export const CACHE_DIRECTORY = join(SENTRIFLOW_HOME, 'cache');
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
includesIgnoreCase,
|
|
11
11
|
startsWithIgnoreCase,
|
|
12
12
|
parseInteger,
|
|
13
|
+
findParentSection,
|
|
13
14
|
} from '../common/helpers';
|
|
14
15
|
|
|
15
16
|
// Re-export common helpers for convenience
|
|
@@ -214,6 +215,24 @@ export const hasWeakUsernamePassword = (node: ConfigNode): boolean => {
|
|
|
214
215
|
return false;
|
|
215
216
|
};
|
|
216
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Check if password is under a line configuration section (vty, console, aux).
|
|
220
|
+
* Line passwords cannot be encrypted in Cisco IOS - they only support plaintext.
|
|
221
|
+
* Security for these should be enforced via AAA authentication instead.
|
|
222
|
+
*
|
|
223
|
+
* @param ast The full AST (array of ConfigNode)
|
|
224
|
+
* @param node The password node to check
|
|
225
|
+
* @returns true if the password is under a line vty/console/aux section
|
|
226
|
+
*/
|
|
227
|
+
export const isLineConfigPassword = (ast: ConfigNode[], node: ConfigNode): boolean => {
|
|
228
|
+
const parent = findParentSection(ast, node);
|
|
229
|
+
if (!parent) return false;
|
|
230
|
+
const parentId = parent.id.toLowerCase();
|
|
231
|
+
return parentId.startsWith('line vty') ||
|
|
232
|
+
parentId.startsWith('line console') ||
|
|
233
|
+
parentId.startsWith('line aux');
|
|
234
|
+
};
|
|
235
|
+
|
|
217
236
|
/**
|
|
218
237
|
* Get SSH version from configuration
|
|
219
238
|
*/
|
|
@@ -202,6 +202,35 @@ export const isShutdown = (node: ConfigNode): boolean => {
|
|
|
202
202
|
});
|
|
203
203
|
};
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Find the parent section of a node in the AST.
|
|
207
|
+
* Traverses the AST to locate the parent section containing the target node.
|
|
208
|
+
* Useful for context-aware rules that need to check parent context.
|
|
209
|
+
*
|
|
210
|
+
* @param ast The full AST (array of ConfigNode)
|
|
211
|
+
* @param targetNode The node to find the parent for
|
|
212
|
+
* @returns The parent section node, or undefined if not found
|
|
213
|
+
*/
|
|
214
|
+
export const findParentSection = (
|
|
215
|
+
ast: ConfigNode[],
|
|
216
|
+
targetNode: ConfigNode
|
|
217
|
+
): ConfigNode | undefined => {
|
|
218
|
+
for (const node of ast) {
|
|
219
|
+
if (node.type === 'section') {
|
|
220
|
+
// Check if target is a direct child (by line number match)
|
|
221
|
+
if (node.children.some(child =>
|
|
222
|
+
child.loc.startLine === targetNode.loc.startLine
|
|
223
|
+
)) {
|
|
224
|
+
return node;
|
|
225
|
+
}
|
|
226
|
+
// Recurse into children
|
|
227
|
+
const found = findParentSection(node.children, targetNode);
|
|
228
|
+
if (found) return found;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
};
|
|
233
|
+
|
|
205
234
|
/**
|
|
206
235
|
* Check if a node is an actual interface definition (not a reference or sub-command).
|
|
207
236
|
* Interface definitions are top-level sections that define physical/logical interfaces.
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// packages/core/src/ip/classifier.ts
|
|
2
|
+
|
|
3
|
+
import type { IPSummary, IPCounts } from './types';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// IP Classification Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Classification categories for IP addresses.
|
|
11
|
+
*/
|
|
12
|
+
export type IPClassification =
|
|
13
|
+
| 'public' // Globally routable
|
|
14
|
+
| 'private' // RFC 1918 private ranges
|
|
15
|
+
| 'loopback' // 127.0.0.0/8, ::1
|
|
16
|
+
| 'link-local' // 169.254.0.0/16, fe80::/10
|
|
17
|
+
| 'multicast' // 224.0.0.0/4, ff00::/8
|
|
18
|
+
| 'reserved' // 240.0.0.0/4, other reserved
|
|
19
|
+
| 'unspecified' // 0.0.0.0, ::
|
|
20
|
+
| 'broadcast' // 255.255.255.255
|
|
21
|
+
| 'documentation' // 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32
|
|
22
|
+
| 'cgnat'; // 100.64.0.0/10 (Carrier-grade NAT)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options for filtering IP addresses.
|
|
26
|
+
*/
|
|
27
|
+
export interface IPFilterOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Keep public (globally routable) addresses.
|
|
30
|
+
* @default true
|
|
31
|
+
*/
|
|
32
|
+
keepPublic?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Keep private (RFC 1918) addresses.
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
keepPrivate?: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Keep loopback addresses (127.x.x.x, ::1).
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
keepLoopback?: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Keep link-local addresses (169.254.x.x, fe80::).
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
keepLinkLocal?: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Keep multicast addresses (224.x.x.x - 239.x.x.x, ff00::).
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
keepMulticast?: boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Keep reserved/future use addresses (240.x.x.x - 255.x.x.x).
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
keepReserved?: boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Keep unspecified addresses (0.0.0.0, ::).
|
|
66
|
+
* @default false
|
|
67
|
+
*/
|
|
68
|
+
keepUnspecified?: boolean;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Keep broadcast address (255.255.255.255).
|
|
72
|
+
* @default false
|
|
73
|
+
*/
|
|
74
|
+
keepBroadcast?: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Keep documentation addresses (TEST-NET ranges, 2001:db8::).
|
|
78
|
+
* @default false
|
|
79
|
+
*/
|
|
80
|
+
keepDocumentation?: boolean;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Keep CGNAT addresses (100.64.0.0/10).
|
|
84
|
+
* @default true
|
|
85
|
+
*/
|
|
86
|
+
keepCgnat?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Default filter options - keep only public and private addresses.
|
|
91
|
+
*/
|
|
92
|
+
export const DEFAULT_FILTER_OPTIONS: Required<IPFilterOptions> = {
|
|
93
|
+
keepPublic: true,
|
|
94
|
+
keepPrivate: true,
|
|
95
|
+
keepLoopback: false,
|
|
96
|
+
keepLinkLocal: false,
|
|
97
|
+
keepMulticast: false,
|
|
98
|
+
keepReserved: false,
|
|
99
|
+
keepUnspecified: false,
|
|
100
|
+
keepBroadcast: false,
|
|
101
|
+
keepDocumentation: false,
|
|
102
|
+
keepCgnat: true,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// IPv4 Classification
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convert IPv4 address string to 32-bit number.
|
|
111
|
+
*/
|
|
112
|
+
function ipv4ToNumber(ip: string): number {
|
|
113
|
+
const parts = ip.split('.');
|
|
114
|
+
if (parts.length !== 4) return 0;
|
|
115
|
+
|
|
116
|
+
let result = 0;
|
|
117
|
+
for (let i = 0; i < 4; i++) {
|
|
118
|
+
const octet = parseInt(parts[i] ?? '0', 10);
|
|
119
|
+
if (isNaN(octet) || octet < 0 || octet > 255) return 0;
|
|
120
|
+
result = (result << 8) + octet;
|
|
121
|
+
}
|
|
122
|
+
return result >>> 0; // Ensure unsigned
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if IPv4 address is in a given CIDR range.
|
|
127
|
+
*/
|
|
128
|
+
function isInIPv4Range(ip: string, network: string, prefix: number): boolean {
|
|
129
|
+
const ipNum = ipv4ToNumber(ip);
|
|
130
|
+
const netNum = ipv4ToNumber(network);
|
|
131
|
+
const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
|
|
132
|
+
return (ipNum & mask) === (netNum & mask);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Classify an IPv4 address.
|
|
137
|
+
*/
|
|
138
|
+
export function classifyIPv4(ip: string): IPClassification {
|
|
139
|
+
// Unspecified
|
|
140
|
+
if (ip === '0.0.0.0') return 'unspecified';
|
|
141
|
+
|
|
142
|
+
// Broadcast
|
|
143
|
+
if (ip === '255.255.255.255') return 'broadcast';
|
|
144
|
+
|
|
145
|
+
// Current network (0.0.0.0/8) - treat as unspecified
|
|
146
|
+
if (isInIPv4Range(ip, '0.0.0.0', 8)) return 'unspecified';
|
|
147
|
+
|
|
148
|
+
// Loopback (127.0.0.0/8)
|
|
149
|
+
if (isInIPv4Range(ip, '127.0.0.0', 8)) return 'loopback';
|
|
150
|
+
|
|
151
|
+
// Link-local (169.254.0.0/16)
|
|
152
|
+
if (isInIPv4Range(ip, '169.254.0.0', 16)) return 'link-local';
|
|
153
|
+
|
|
154
|
+
// Private ranges (RFC 1918)
|
|
155
|
+
if (isInIPv4Range(ip, '10.0.0.0', 8)) return 'private';
|
|
156
|
+
if (isInIPv4Range(ip, '172.16.0.0', 12)) return 'private';
|
|
157
|
+
if (isInIPv4Range(ip, '192.168.0.0', 16)) return 'private';
|
|
158
|
+
|
|
159
|
+
// CGNAT (100.64.0.0/10)
|
|
160
|
+
if (isInIPv4Range(ip, '100.64.0.0', 10)) return 'cgnat';
|
|
161
|
+
|
|
162
|
+
// Documentation ranges (TEST-NET)
|
|
163
|
+
if (isInIPv4Range(ip, '192.0.2.0', 24)) return 'documentation';
|
|
164
|
+
if (isInIPv4Range(ip, '198.51.100.0', 24)) return 'documentation';
|
|
165
|
+
if (isInIPv4Range(ip, '203.0.113.0', 24)) return 'documentation';
|
|
166
|
+
|
|
167
|
+
// Multicast (224.0.0.0/4)
|
|
168
|
+
if (isInIPv4Range(ip, '224.0.0.0', 4)) return 'multicast';
|
|
169
|
+
|
|
170
|
+
// Reserved for future use (240.0.0.0/4)
|
|
171
|
+
if (isInIPv4Range(ip, '240.0.0.0', 4)) return 'reserved';
|
|
172
|
+
|
|
173
|
+
// Everything else is public
|
|
174
|
+
return 'public';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Classify an IPv4 subnet (uses network address for classification).
|
|
179
|
+
*/
|
|
180
|
+
export function classifyIPv4Subnet(subnet: string): IPClassification {
|
|
181
|
+
const slashIndex = subnet.lastIndexOf('/');
|
|
182
|
+
if (slashIndex === -1) return classifyIPv4(subnet);
|
|
183
|
+
|
|
184
|
+
const network = subnet.substring(0, slashIndex);
|
|
185
|
+
return classifyIPv4(network);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// IPv6 Classification
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Expand IPv6 address to full form and return as array of 16-bit values.
|
|
194
|
+
*/
|
|
195
|
+
function expandIPv6(ip: string): number[] {
|
|
196
|
+
// Strip zone ID if present
|
|
197
|
+
const zoneIndex = ip.indexOf('%');
|
|
198
|
+
const addr = zoneIndex !== -1 ? ip.substring(0, zoneIndex) : ip;
|
|
199
|
+
|
|
200
|
+
const parts = addr.split(':');
|
|
201
|
+
const result: number[] = [];
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < parts.length; i++) {
|
|
204
|
+
const part = parts[i] ?? '';
|
|
205
|
+
if (part === '' && i > 0 && i < parts.length - 1) {
|
|
206
|
+
// Middle :: - expand
|
|
207
|
+
const nonEmpty = parts.filter((p) => p !== '').length;
|
|
208
|
+
const zeros = 8 - nonEmpty;
|
|
209
|
+
for (let j = 0; j < zeros; j++) {
|
|
210
|
+
result.push(0);
|
|
211
|
+
}
|
|
212
|
+
} else if (part !== '') {
|
|
213
|
+
result.push(parseInt(part, 16) || 0);
|
|
214
|
+
} else if (i === 0 && (parts[1] ?? '') === '') {
|
|
215
|
+
// Leading ::
|
|
216
|
+
const nonEmpty = parts.filter((p) => p !== '').length;
|
|
217
|
+
const zeros = 8 - nonEmpty;
|
|
218
|
+
for (let j = 0; j < zeros; j++) {
|
|
219
|
+
result.push(0);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Pad to 8 if needed
|
|
225
|
+
while (result.length < 8) {
|
|
226
|
+
result.push(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return result.slice(0, 8);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if IPv6 starts with a specific prefix.
|
|
234
|
+
*/
|
|
235
|
+
function ipv6StartsWith(ip: string, prefixHex: number, prefixBits: number): boolean {
|
|
236
|
+
const parts = expandIPv6(ip);
|
|
237
|
+
|
|
238
|
+
// Calculate how many 16-bit groups we need to check
|
|
239
|
+
const fullGroups = Math.floor(prefixBits / 16);
|
|
240
|
+
const remainingBits = prefixBits % 16;
|
|
241
|
+
|
|
242
|
+
// Build the prefix value from parts
|
|
243
|
+
let value = 0;
|
|
244
|
+
for (let i = 0; i < fullGroups && i < parts.length; i++) {
|
|
245
|
+
value = (value << 16) | (parts[i] ?? 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (remainingBits > 0 && fullGroups < parts.length) {
|
|
249
|
+
const mask = (~0 << (16 - remainingBits)) & 0xffff;
|
|
250
|
+
value = (value << remainingBits) | (((parts[fullGroups] ?? 0) & mask) >> (16 - remainingBits));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return value === prefixHex;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Classify an IPv6 address.
|
|
258
|
+
*/
|
|
259
|
+
export function classifyIPv6(ip: string): IPClassification {
|
|
260
|
+
const parts = expandIPv6(ip);
|
|
261
|
+
|
|
262
|
+
// Unspecified (::)
|
|
263
|
+
if (parts.every((p) => p === 0)) return 'unspecified';
|
|
264
|
+
|
|
265
|
+
// Loopback (::1)
|
|
266
|
+
if (parts.slice(0, 7).every((p) => p === 0) && parts[7] === 1) return 'loopback';
|
|
267
|
+
|
|
268
|
+
// Link-local (fe80::/10)
|
|
269
|
+
if ((parts[0] ?? 0) >= 0xfe80 && (parts[0] ?? 0) <= 0xfebf) return 'link-local';
|
|
270
|
+
|
|
271
|
+
// Multicast (ff00::/8)
|
|
272
|
+
if (((parts[0] ?? 0) & 0xff00) === 0xff00) return 'multicast';
|
|
273
|
+
|
|
274
|
+
// Documentation (2001:db8::/32)
|
|
275
|
+
if (parts[0] === 0x2001 && parts[1] === 0x0db8) return 'documentation';
|
|
276
|
+
|
|
277
|
+
// Unique local (fc00::/7) - similar to private
|
|
278
|
+
if (((parts[0] ?? 0) & 0xfe00) === 0xfc00) return 'private';
|
|
279
|
+
|
|
280
|
+
// Everything else is public
|
|
281
|
+
return 'public';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Classify an IPv6 subnet (uses network address for classification).
|
|
286
|
+
*/
|
|
287
|
+
export function classifyIPv6Subnet(subnet: string): IPClassification {
|
|
288
|
+
const slashIndex = subnet.lastIndexOf('/');
|
|
289
|
+
if (slashIndex === -1) return classifyIPv6(subnet);
|
|
290
|
+
|
|
291
|
+
const network = subnet.substring(0, slashIndex);
|
|
292
|
+
return classifyIPv6(network);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Filtering Functions
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check if a classification should be kept based on filter options.
|
|
301
|
+
*/
|
|
302
|
+
function shouldKeepClassification(
|
|
303
|
+
classification: IPClassification,
|
|
304
|
+
options: Required<IPFilterOptions>
|
|
305
|
+
): boolean {
|
|
306
|
+
switch (classification) {
|
|
307
|
+
case 'public':
|
|
308
|
+
return options.keepPublic;
|
|
309
|
+
case 'private':
|
|
310
|
+
return options.keepPrivate;
|
|
311
|
+
case 'loopback':
|
|
312
|
+
return options.keepLoopback;
|
|
313
|
+
case 'link-local':
|
|
314
|
+
return options.keepLinkLocal;
|
|
315
|
+
case 'multicast':
|
|
316
|
+
return options.keepMulticast;
|
|
317
|
+
case 'reserved':
|
|
318
|
+
return options.keepReserved;
|
|
319
|
+
case 'unspecified':
|
|
320
|
+
return options.keepUnspecified;
|
|
321
|
+
case 'broadcast':
|
|
322
|
+
return options.keepBroadcast;
|
|
323
|
+
case 'documentation':
|
|
324
|
+
return options.keepDocumentation;
|
|
325
|
+
case 'cgnat':
|
|
326
|
+
return options.keepCgnat;
|
|
327
|
+
default:
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Filter an array of IPv4 addresses based on classification.
|
|
334
|
+
*/
|
|
335
|
+
export function filterIPv4Addresses(
|
|
336
|
+
addresses: string[],
|
|
337
|
+
options: IPFilterOptions = {}
|
|
338
|
+
): string[] {
|
|
339
|
+
const opts: Required<IPFilterOptions> = { ...DEFAULT_FILTER_OPTIONS, ...options };
|
|
340
|
+
return addresses.filter((ip) => {
|
|
341
|
+
const classification = classifyIPv4(ip);
|
|
342
|
+
return shouldKeepClassification(classification, opts);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Filter an array of IPv6 addresses based on classification.
|
|
348
|
+
*/
|
|
349
|
+
export function filterIPv6Addresses(
|
|
350
|
+
addresses: string[],
|
|
351
|
+
options: IPFilterOptions = {}
|
|
352
|
+
): string[] {
|
|
353
|
+
const opts: Required<IPFilterOptions> = { ...DEFAULT_FILTER_OPTIONS, ...options };
|
|
354
|
+
return addresses.filter((ip) => {
|
|
355
|
+
const classification = classifyIPv6(ip);
|
|
356
|
+
return shouldKeepClassification(classification, opts);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Filter an array of IPv4 subnets based on classification.
|
|
362
|
+
*/
|
|
363
|
+
export function filterIPv4Subnets(
|
|
364
|
+
subnets: string[],
|
|
365
|
+
options: IPFilterOptions = {}
|
|
366
|
+
): string[] {
|
|
367
|
+
const opts: Required<IPFilterOptions> = { ...DEFAULT_FILTER_OPTIONS, ...options };
|
|
368
|
+
return subnets.filter((subnet) => {
|
|
369
|
+
const classification = classifyIPv4Subnet(subnet);
|
|
370
|
+
return shouldKeepClassification(classification, opts);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Filter an array of IPv6 subnets based on classification.
|
|
376
|
+
*/
|
|
377
|
+
export function filterIPv6Subnets(
|
|
378
|
+
subnets: string[],
|
|
379
|
+
options: IPFilterOptions = {}
|
|
380
|
+
): string[] {
|
|
381
|
+
const opts: Required<IPFilterOptions> = { ...DEFAULT_FILTER_OPTIONS, ...options };
|
|
382
|
+
return subnets.filter((subnet) => {
|
|
383
|
+
const classification = classifyIPv6Subnet(subnet);
|
|
384
|
+
return shouldKeepClassification(classification, opts);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Filter an entire IPSummary based on classification options.
|
|
390
|
+
* Returns a new IPSummary with filtered results and updated counts.
|
|
391
|
+
*/
|
|
392
|
+
export function filterIPSummary(
|
|
393
|
+
summary: IPSummary,
|
|
394
|
+
options: IPFilterOptions = {}
|
|
395
|
+
): IPSummary {
|
|
396
|
+
const ipv4Addresses = filterIPv4Addresses(summary.ipv4Addresses, options);
|
|
397
|
+
const ipv6Addresses = filterIPv6Addresses(summary.ipv6Addresses, options);
|
|
398
|
+
const ipv4Subnets = filterIPv4Subnets(summary.ipv4Subnets, options);
|
|
399
|
+
const ipv6Subnets = filterIPv6Subnets(summary.ipv6Subnets, options);
|
|
400
|
+
|
|
401
|
+
const counts: IPCounts = {
|
|
402
|
+
ipv4: ipv4Addresses.length,
|
|
403
|
+
ipv6: ipv6Addresses.length,
|
|
404
|
+
ipv4Subnets: ipv4Subnets.length,
|
|
405
|
+
ipv6Subnets: ipv6Subnets.length,
|
|
406
|
+
total: ipv4Addresses.length + ipv6Addresses.length + ipv4Subnets.length + ipv6Subnets.length,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
ipv4Addresses,
|
|
411
|
+
ipv6Addresses,
|
|
412
|
+
ipv4Subnets,
|
|
413
|
+
ipv6Subnets,
|
|
414
|
+
counts,
|
|
415
|
+
};
|
|
416
|
+
}
|
package/src/ip/index.ts
CHANGED
|
@@ -25,3 +25,19 @@ export type {
|
|
|
25
25
|
} from './types';
|
|
26
26
|
|
|
27
27
|
export { InputValidationError, DEFAULT_MAX_CONTENT_SIZE } from './types';
|
|
28
|
+
|
|
29
|
+
// IP Classification and Filtering
|
|
30
|
+
export {
|
|
31
|
+
classifyIPv4,
|
|
32
|
+
classifyIPv6,
|
|
33
|
+
classifyIPv4Subnet,
|
|
34
|
+
classifyIPv6Subnet,
|
|
35
|
+
filterIPv4Addresses,
|
|
36
|
+
filterIPv6Addresses,
|
|
37
|
+
filterIPv4Subnets,
|
|
38
|
+
filterIPv6Subnets,
|
|
39
|
+
filterIPSummary,
|
|
40
|
+
DEFAULT_FILTER_OPTIONS,
|
|
41
|
+
} from './classifier';
|
|
42
|
+
|
|
43
|
+
export type { IPClassification, IPFilterOptions } from './classifier';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://sentriflow.
|
|
3
|
+
"$id": "https://sentriflow.com.au/schemas/json-rules/v1.0.json",
|
|
4
4
|
"title": "SentriFlow JSON Rules",
|
|
5
5
|
"description": "Schema for SentriFlow JSON rule files",
|
|
6
6
|
"type": "object",
|
package/src/pack-loader/index.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export * from './types';
|
|
17
|
-
export { loadEncryptedPack, validatePackFormat } from './PackLoader';
|
|
17
|
+
export { loadEncryptedPack, validatePackFormat, compileNativeCheckFunction } from './PackLoader';
|
|
18
18
|
|
|
19
19
|
// Pack format detection (shared with CLI and VS Code)
|
|
20
20
|
export {
|
|
@@ -39,7 +39,7 @@ export interface PackProviderLicenseStatus {
|
|
|
39
39
|
isValid: boolean;
|
|
40
40
|
|
|
41
41
|
/** License tier */
|
|
42
|
-
tier: '
|
|
42
|
+
tier: 'basic' | 'professional' | 'enterprise' | string;
|
|
43
43
|
|
|
44
44
|
/** List of entitled feed/pack IDs */
|
|
45
45
|
entitledFeeds: string[];
|