Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/socket-patch-core/src/crawlers/go_crawler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,10 @@ impl GoCrawler {
_dir_name: &str,
seen: &mut HashSet<String>,
) -> Option<CrawledPackage> {
// Get the relative path from the cache root
// Get the relative path from the cache root.
// Normalize to forward slashes so PURLs are correct on Windows.
let rel_path = dir_path.strip_prefix(base_path).ok()?;
let rel_str = rel_path.to_string_lossy();
let rel_str = rel_path.to_string_lossy().replace('\\', "/");

// Find the last `@` to split module path and version
let at_idx = rel_str.rfind('@')?;
Expand Down
20 changes: 19 additions & 1 deletion npm/socket-patch/package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
{
"name": "@socketsecurity/socket-patch",
"version": "1.6.3",
"description": "CLI tool for applying security patches to dependencies",
"description": "CLI tool and schema library for applying security patches to dependencies",
"bin": {
"socket-patch": "bin/socket-patch"
},
"exports": {
"./schema": {
"types": "./dist/schema/manifest-schema.d.ts",
"import": "./dist/schema/manifest-schema.js",
"require": "./dist/schema/manifest-schema.js"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc",
"test": "pnpm run build && node --test dist/**/*.test.js"
},
"keywords": [
"security",
"patch",
Expand All @@ -23,6 +34,13 @@
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"zod": "^3.24.4"
},
"devDependencies": {
"typescript": "^5.3.0",
"@types/node": "^20.0.0"
},
"optionalDependencies": {
"@socketsecurity/socket-patch-android-arm64": "1.6.3",
"@socketsecurity/socket-patch-darwin-arm64": "1.6.3",
Expand Down
131 changes: 131 additions & 0 deletions npm/socket-patch/src/schema/manifest-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { describe, it } from 'node:test'
import * as assert from 'node:assert/strict'
import { PatchManifestSchema, PatchRecordSchema } from './manifest-schema.js'

describe('PatchManifestSchema', () => {
it('should validate a well-formed manifest', () => {
const manifest = {
patches: {
'npm:simplehttpserver@0.0.6': {
uuid: '550e8400-e29b-41d4-a716-446655440000',
exportedAt: '2024-01-01T00:00:00Z',
files: {
'node_modules/simplehttpserver/index.js': {
beforeHash: 'abc123',
afterHash: 'def456',
},
},
vulnerabilities: {
'GHSA-jrhj-2j3q-xf3v': {
cves: ['CVE-2024-0001'],
summary: 'Path traversal vulnerability',
severity: 'high',
description: 'Allows reading arbitrary files',
},
},
description: 'Fix path traversal',
license: 'MIT',
tier: 'free',
},
},
}

const result = PatchManifestSchema.safeParse(manifest)
assert.ok(result.success, 'Valid manifest should parse successfully')
assert.equal(
Object.keys(result.data.patches).length,
1,
'Should have one patch entry',
)
})

it('should validate a manifest with multiple patches', () => {
const manifest = {
patches: {
'npm:pkg-a@1.0.0': {
uuid: '550e8400-e29b-41d4-a716-446655440001',
exportedAt: '2024-01-01T00:00:00Z',
files: {
'node_modules/pkg-a/lib/index.js': {
beforeHash: 'aaa',
afterHash: 'bbb',
},
},
vulnerabilities: {},
description: 'Patch A',
license: 'MIT',
tier: 'free',
},
'npm:pkg-b@2.0.0': {
uuid: '550e8400-e29b-41d4-a716-446655440002',
exportedAt: '2024-02-01T00:00:00Z',
files: {
'node_modules/pkg-b/src/main.js': {
beforeHash: 'ccc',
afterHash: 'ddd',
},
},
vulnerabilities: {
'GHSA-xxxx-yyyy-zzzz': {
cves: [],
summary: 'Some vuln',
severity: 'medium',
description: 'A medium severity vulnerability',
},
},
description: 'Patch B',
license: 'Apache-2.0',
tier: 'paid',
},
},
}

const result = PatchManifestSchema.safeParse(manifest)
assert.ok(result.success, 'Multi-patch manifest should parse successfully')
assert.equal(Object.keys(result.data.patches).length, 2)
})

it('should validate an empty manifest', () => {
const manifest = { patches: {} }
const result = PatchManifestSchema.safeParse(manifest)
assert.ok(result.success, 'Empty patches should be valid')
})

it('should reject a manifest missing the patches field', () => {
const result = PatchManifestSchema.safeParse({})
assert.ok(!result.success, 'Missing patches should fail')
})

it('should reject a manifest with invalid patch record', () => {
const manifest = {
patches: {
'npm:bad@1.0.0': {
// missing uuid, exportedAt, files, vulnerabilities, description, license, tier
},
},
}
const result = PatchManifestSchema.safeParse(manifest)
assert.ok(!result.success, 'Invalid patch record should fail')
})

it('should reject a patch with invalid uuid', () => {
const record = {
uuid: 'not-a-valid-uuid',
exportedAt: '2024-01-01T00:00:00Z',
files: {},
vulnerabilities: {},
description: 'Test',
license: 'MIT',
tier: 'free',
}
const result = PatchRecordSchema.safeParse(record)
assert.ok(!result.success, 'Invalid UUID should fail')
})

it('should reject non-object input', () => {
assert.ok(!PatchManifestSchema.safeParse(null).success)
assert.ok(!PatchManifestSchema.safeParse('string').success)
assert.ok(!PatchManifestSchema.safeParse(42).success)
assert.ok(!PatchManifestSchema.safeParse([]).success)
})
})
38 changes: 38 additions & 0 deletions npm/socket-patch/src/schema/manifest-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from 'zod'

export const DEFAULT_PATCH_MANIFEST_PATH = '.socket/manifest.json'

export const PatchRecordSchema = z.object({
uuid: z.string().uuid(),
exportedAt: z.string(),
files: z.record(
z.string(), // File path
z.object({
beforeHash: z.string(),
afterHash: z.string(),
}),
),
vulnerabilities: z.record(
z.string(), // Vulnerability ID like "GHSA-jrhj-2j3q-xf3v"
z.object({
cves: z.array(z.string()),
summary: z.string(),
severity: z.string(),
description: z.string(),
}),
),
description: z.string(),
license: z.string(),
tier: z.string(),
})

export type PatchRecord = z.infer<typeof PatchRecordSchema>

export const PatchManifestSchema = z.object({
patches: z.record(
z.string(), // Package identifier like "npm:simplehttpserver@0.0.6"
PatchRecordSchema,
),
})

export type PatchManifest = z.infer<typeof PatchManifestSchema>
15 changes: 15 additions & 0 deletions npm/socket-patch/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"declaration": true,
"composite": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"include": ["src"]
}