@sentriflow/core 0.2.1 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "SentriFlow core engine for network configuration validation",
5
5
  "license": "Apache-2.0",
6
6
  "module": "src/index.ts",
@@ -189,6 +189,9 @@ export interface EncryptedPackInfo {
189
189
 
190
190
  /** Source: local directory or cloud cache */
191
191
  source: 'local' | 'cache';
192
+
193
+ /** Pack format (for unified loading) */
194
+ format?: 'grx2' | 'grpx' | 'unencrypted' | 'unknown';
192
195
  }
193
196
 
194
197
  /**
package/src/index.ts CHANGED
@@ -37,3 +37,6 @@ export * from './helpers/common';
37
37
 
38
38
  // IP/Subnet extraction module
39
39
  export * from './ip';
40
+
41
+ // Validation utilities (shared with CLI and VS Code)
42
+ export * from './validation';
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Pack Format Detection
3
+ *
4
+ * Detects pack format from magic bytes.
5
+ * Shared between CLI and VS Code extension.
6
+ *
7
+ * @module format-detector
8
+ */
9
+
10
+ import { open } from 'node:fs/promises';
11
+ import { resolve } from 'node:path';
12
+
13
+ /**
14
+ * Detected pack format
15
+ */
16
+ export type PackFormat = 'grx2' | 'grpx' | 'unencrypted' | 'unknown';
17
+
18
+ /**
19
+ * Priority tiers by format.
20
+ * Higher priority packs override lower priority packs for the same rule.
21
+ *
22
+ * - unknown: 0 (fallback, should not occur in normal operation)
23
+ * - unencrypted: 100 (plain JS/TS modules)
24
+ * - grpx: 200 (legacy encrypted format)
25
+ * - grx2: 300 (extended encrypted format)
26
+ */
27
+ export const FORMAT_PRIORITIES: Record<PackFormat, number> = {
28
+ unknown: 0,
29
+ unencrypted: 100,
30
+ grpx: 200,
31
+ grx2: 300,
32
+ };
33
+
34
+ /**
35
+ * Magic bytes for format detection
36
+ */
37
+ const MAGIC_BYTES = {
38
+ GRX2: Buffer.from('GRX2', 'ascii'),
39
+ GRPX: Buffer.from('GRPX', 'ascii'),
40
+ } as const;
41
+
42
+ const MAGIC_BYTES_LENGTH = 4;
43
+
44
+ /**
45
+ * Detect the format of a pack file by reading magic bytes.
46
+ *
47
+ * @param filePath - Path to the pack file
48
+ * @returns Promise resolving to the detected format
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * import { detectPackFormat } from '@sentriflow/core';
53
+ *
54
+ * const format = await detectPackFormat('/path/to/pack.grx2');
55
+ * // Returns: 'grx2'
56
+ *
57
+ * const format2 = await detectPackFormat('/path/to/rules.js');
58
+ * // Returns: 'unencrypted'
59
+ * ```
60
+ */
61
+ export async function detectPackFormat(filePath: string): Promise<PackFormat> {
62
+ const absolutePath = resolve(filePath);
63
+ let fileHandle;
64
+
65
+ try {
66
+ fileHandle = await open(absolutePath, 'r');
67
+ const stats = await fileHandle.stat();
68
+
69
+ // Files smaller than magic bytes length are treated as unencrypted
70
+ if (stats.size < MAGIC_BYTES_LENGTH) {
71
+ return 'unencrypted';
72
+ }
73
+
74
+ const buffer = Buffer.alloc(MAGIC_BYTES_LENGTH);
75
+ const { bytesRead } = await fileHandle.read(
76
+ buffer,
77
+ 0,
78
+ MAGIC_BYTES_LENGTH,
79
+ 0
80
+ );
81
+
82
+ if (bytesRead < MAGIC_BYTES_LENGTH) {
83
+ return 'unencrypted';
84
+ }
85
+
86
+ // Check for GRX2 magic bytes
87
+ if (buffer.equals(MAGIC_BYTES.GRX2)) {
88
+ return 'grx2';
89
+ }
90
+
91
+ // Check for GRPX magic bytes
92
+ if (buffer.equals(MAGIC_BYTES.GRPX)) {
93
+ return 'grpx';
94
+ }
95
+
96
+ // No magic bytes match - treat as unencrypted module
97
+ return 'unencrypted';
98
+ } catch (error) {
99
+ // Re-throw file access errors with context
100
+ if (error instanceof Error) {
101
+ const nodeError = error as NodeJS.ErrnoException;
102
+ if (nodeError.code === 'ENOENT') {
103
+ throw new Error(`Pack file not found: ${absolutePath}`);
104
+ }
105
+ if (nodeError.code === 'EACCES') {
106
+ throw new Error(`Permission denied reading pack file: ${absolutePath}`);
107
+ }
108
+ }
109
+ throw error;
110
+ } finally {
111
+ await fileHandle?.close();
112
+ }
113
+ }
@@ -15,3 +15,10 @@
15
15
 
16
16
  export * from './types';
17
17
  export { loadEncryptedPack, validatePackFormat } from './PackLoader';
18
+
19
+ // Pack format detection (shared with CLI and VS Code)
20
+ export {
21
+ detectPackFormat,
22
+ FORMAT_PRIORITIES,
23
+ type PackFormat,
24
+ } from './format-detector';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Validation utilities for rules and rule packs.
3
+ * Shared between CLI and VS Code extension.
4
+ */
5
+
6
+ export {
7
+ validateRule,
8
+ isValidRule,
9
+ validateRulePack,
10
+ isValidRulePack,
11
+ ruleAppliesToVendor,
12
+ } from './rule-validation';
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Rule and RulePack validation utilities
3
+ * Shared between CLI and VS Code extension for DRY compliance.
4
+ */
5
+
6
+ import { RULE_ID_PATTERN } from '../constants';
7
+ import { isValidVendorId } from '../types/IRule';
8
+ import type { IRule, RulePack, RuleVendor } from '../types/IRule';
9
+
10
+ /**
11
+ * Validates that an object has the basic structure of an IRule.
12
+ * Returns error message if invalid, null if valid.
13
+ *
14
+ * Security: Prevents malicious extensions from registering invalid rules.
15
+ */
16
+ export function validateRule(rule: unknown): string | null {
17
+ if (typeof rule !== 'object' || rule === null) {
18
+ return 'Rule is not an object';
19
+ }
20
+
21
+ const obj = rule as Record<string, unknown>;
22
+
23
+ // Required: id (string matching pattern)
24
+ if (typeof obj.id !== 'string') {
25
+ return 'Rule id is not a string';
26
+ }
27
+ if (!RULE_ID_PATTERN.test(obj.id)) {
28
+ return `Rule id "${obj.id}" does not match pattern ${RULE_ID_PATTERN}`;
29
+ }
30
+
31
+ // Required: check (function)
32
+ if (typeof obj.check !== 'function') {
33
+ return `Rule ${obj.id}: check is not a function (got ${typeof obj.check})`;
34
+ }
35
+
36
+ // Optional but recommended: selector (string)
37
+ if (obj.selector !== undefined && typeof obj.selector !== 'string') {
38
+ return `Rule ${obj.id}: selector is not a string`;
39
+ }
40
+
41
+ // Optional: vendor (string or array of valid vendors)
42
+ if (obj.vendor !== undefined) {
43
+ if (Array.isArray(obj.vendor)) {
44
+ for (const v of obj.vendor) {
45
+ if (typeof v !== 'string') {
46
+ return `Rule ${obj.id}: vendor array contains non-string`;
47
+ }
48
+ if (!isValidVendorId(v)) {
49
+ return `Rule ${obj.id}: invalid vendor "${v}"`;
50
+ }
51
+ }
52
+ } else if (typeof obj.vendor !== 'string') {
53
+ return `Rule ${obj.id}: vendor is not a string`;
54
+ } else if (!isValidVendorId(obj.vendor)) {
55
+ return `Rule ${obj.id}: invalid vendor "${obj.vendor}"`;
56
+ }
57
+ }
58
+
59
+ // Required: metadata (object with level)
60
+ if (typeof obj.metadata !== 'object' || obj.metadata === null) {
61
+ return `Rule ${obj.id}: metadata is not an object`;
62
+ }
63
+
64
+ const metadata = obj.metadata as Record<string, unknown>;
65
+ if (!['error', 'warning', 'info'].includes(metadata.level as string)) {
66
+ return `Rule ${obj.id}: invalid metadata.level "${metadata.level}"`;
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Type guard to check if an object is a valid IRule.
74
+ */
75
+ export function isValidRule(rule: unknown): rule is IRule {
76
+ return validateRule(rule) === null;
77
+ }
78
+
79
+ /**
80
+ * Validates that an object has the basic structure of a RulePack.
81
+ * Returns error message if invalid, null if valid.
82
+ *
83
+ * @param pack - The object to validate
84
+ * @param reservedPackName - Optional pack name that is reserved (e.g., "Default Rules")
85
+ */
86
+ export function validateRulePack(
87
+ pack: unknown,
88
+ reservedPackName?: string
89
+ ): string | null {
90
+ if (typeof pack !== 'object' || pack === null) {
91
+ return 'Pack is not an object';
92
+ }
93
+
94
+ const obj = pack as Record<string, unknown>;
95
+
96
+ // Required: name (non-empty string)
97
+ if (typeof obj.name !== 'string' || obj.name.length === 0) {
98
+ return 'Pack name is missing or empty';
99
+ }
100
+
101
+ // Check reserved name if provided
102
+ if (reservedPackName && obj.name === reservedPackName) {
103
+ return `Pack name "${obj.name}" is reserved`;
104
+ }
105
+
106
+ // Required: version (string)
107
+ if (typeof obj.version !== 'string' || obj.version.length === 0) {
108
+ return 'Pack version is missing or empty';
109
+ }
110
+
111
+ // Required: publisher (string)
112
+ if (typeof obj.publisher !== 'string' || obj.publisher.length === 0) {
113
+ return 'Pack publisher is missing or empty';
114
+ }
115
+
116
+ // Required: priority (number >= 0)
117
+ if (typeof obj.priority !== 'number' || obj.priority < 0) {
118
+ return `Pack priority is invalid (got ${obj.priority})`;
119
+ }
120
+
121
+ // Required: rules (array)
122
+ if (!Array.isArray(obj.rules)) {
123
+ return 'Pack rules is not an array';
124
+ }
125
+
126
+ // Validate each rule in the pack
127
+ for (let i = 0; i < obj.rules.length; i++) {
128
+ const ruleError = validateRule(obj.rules[i]);
129
+ if (ruleError) {
130
+ return `Rule[${i}]: ${ruleError}`;
131
+ }
132
+ }
133
+
134
+ // Optional: disables (object with specific structure)
135
+ if (obj.disables !== undefined) {
136
+ if (typeof obj.disables !== 'object' || obj.disables === null) {
137
+ return 'Pack disables is not an object';
138
+ }
139
+
140
+ const disables = obj.disables as Record<string, unknown>;
141
+
142
+ // Optional: all (boolean)
143
+ if (disables.all !== undefined && typeof disables.all !== 'boolean') {
144
+ return 'Pack disables.all is not a boolean';
145
+ }
146
+
147
+ // Optional: vendors (array of valid vendor strings)
148
+ if (disables.vendors !== undefined) {
149
+ if (!Array.isArray(disables.vendors)) {
150
+ return 'Pack disables.vendors is not an array';
151
+ }
152
+ for (const v of disables.vendors) {
153
+ if (typeof v !== 'string' || !isValidVendorId(v)) {
154
+ return `Pack disables.vendors contains invalid vendor "${v}"`;
155
+ }
156
+ }
157
+ }
158
+
159
+ // Optional: rules (array of strings)
160
+ if (disables.rules !== undefined) {
161
+ if (!Array.isArray(disables.rules)) {
162
+ return 'Pack disables.rules is not an array';
163
+ }
164
+ for (const r of disables.rules) {
165
+ if (typeof r !== 'string') {
166
+ return 'Pack disables.rules contains non-string';
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ return null;
173
+ }
174
+
175
+ /**
176
+ * Type guard to check if an object is a valid RulePack.
177
+ *
178
+ * @param pack - The object to validate
179
+ * @param reservedPackName - Optional pack name that is reserved
180
+ */
181
+ export function isValidRulePack(
182
+ pack: unknown,
183
+ reservedPackName?: string
184
+ ): pack is RulePack {
185
+ return validateRulePack(pack, reservedPackName) === null;
186
+ }
187
+
188
+ /**
189
+ * Check if a rule applies to the given vendor.
190
+ * Rules without a vendor property are considered vendor-agnostic (apply to all).
191
+ * Rules with vendor: 'common' also apply to all vendors.
192
+ */
193
+ export function ruleAppliesToVendor(rule: IRule, vendorId: string): boolean {
194
+ // No vendor specified = vendor-agnostic, applies to all
195
+ if (!rule.vendor) {
196
+ return true;
197
+ }
198
+
199
+ // Handle array of vendors
200
+ if (Array.isArray(rule.vendor)) {
201
+ return (
202
+ rule.vendor.includes('common') ||
203
+ rule.vendor.includes(vendorId as RuleVendor)
204
+ );
205
+ }
206
+
207
+ // Single vendor
208
+ return rule.vendor === 'common' || rule.vendor === vendorId;
209
+ }