-
Notifications
You must be signed in to change notification settings - Fork 66
Expand file tree
/
Copy pathghsa-tracker.mts
More file actions
176 lines (157 loc) · 5.22 KB
/
Copy pathghsa-tracker.mts
File metadata and controls
176 lines (157 loc) · 5.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { debug, debugDir } from '@socketsecurity/lib-stable/debug/output'
import { readJson } from '@socketsecurity/lib-stable/fs/read-json'
import { safeMkdir } from '@socketsecurity/lib-stable/fs/safe'
import { writeJson } from '@socketsecurity/lib-stable/fs/write-json'
import { getSocketFixBranchName } from './git.mts'
import { strictDelete } from '../../util/fs/strict-delete.mts'
export type GhsaFixRecord = {
branch: string
fixedAt: string // ISO 8601
ghsaId: string
prNumber?: number | undefined
}
export type GhsaTracker = {
fixed: GhsaFixRecord[]
version: 1
}
const TRACKER_FILE = '.socket/fixed-ghsas.json'
/**
* Check if a GHSA has been fixed according to the tracker.
*/
export async function isGhsaFixed(
cwd: string,
ghsaId: string,
): Promise<boolean> {
try {
const tracker = await loadGhsaTracker(cwd)
return tracker.fixed.some(r => r.ghsaId === ghsaId)
} catch (e) {
debug(`ghsa-tracker: failed to check if ${ghsaId} is fixed`)
debugDir(e)
return false
}
}
/**
* Check if a process with the given PID is still running.
*/
export function isPidAlive(pid: number): boolean {
try {
// Signal 0 checks process existence without sending actual signal.
process.kill(pid, 0)
return true
} catch (e) {
const err = e as NodeJS.ErrnoException
// EPERM means process exists but no permission, treat as alive.
// ESRCH means process doesn't exist (dead).
// All other errors (EINVAL, etc.) treat as dead to be safe.
return err.code === 'EPERM'
}
}
/**
* Load the GHSA tracker from the repository. Creates a new tracker if the file
* doesn't exist.
*/
export async function loadGhsaTracker(cwd: string): Promise<GhsaTracker> {
const trackerPath = path.join(cwd, TRACKER_FILE)
try {
const data = await readJson(trackerPath)
return (data as GhsaTracker) ?? { version: 1, fixed: [] }
} catch (_e) {
debug(`ghsa-tracker: creating new tracker at ${trackerPath}`)
return { version: 1, fixed: [] }
}
}
/**
* Mark a GHSA as fixed in the tracker. Removes any existing record for the same
* GHSA before adding the new one. Uses file locking to prevent race conditions
* with concurrent operations.
*/
export async function markGhsaFixed(
cwd: string,
ghsaId: string,
prNumber?: number | undefined,
branch?: string | undefined,
): Promise<void> {
const trackerPath = path.join(cwd, TRACKER_FILE)
const lockFile = `${trackerPath}.lock`
// Acquire lock with exponential backoff and stale lock detection.
let lockAcquired = false
for (let attempt = 0; attempt < 5; attempt++) {
try {
await fs.writeFile(lockFile, String(process.pid), { flag: 'wx' })
lockAcquired = true
break
} catch (e) {
const err = e as NodeJS.ErrnoException
if (err.code === 'EEXIST' && attempt < 4) {
// Lock exists, check if it's stale.
try {
const lockContent = await fs.readFile(lockFile, 'utf8')
const lockPid = Number.parseInt(lockContent.trim(), 10)
if (!Number.isNaN(lockPid) && !isPidAlive(lockPid)) {
// Stale lock detected, remove and retry immediately.
debug(
`ghsa-tracker: removing stale lock from dead process ${lockPid}`,
)
await strictDelete(lockFile)
continue
}
} catch {
// Could not read lock file, may have been removed.
}
// Lock exists and process is alive, wait with exponential backoff.
// Delays: 100ms, 200ms, 400ms, 800ms, capped at 10s to prevent overflow.
await new Promise(resolve =>
setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 10_000)),
)
continue
}
// If not EEXIST or last attempt, proceed without lock.
debug(`ghsa-tracker: could not acquire lock, proceeding anyway`)
break
}
}
try {
const tracker = await loadGhsaTracker(cwd)
// Remove any existing record for this GHSA.
tracker.fixed = tracker.fixed.filter(r => r.ghsaId !== ghsaId)
// Add new record.
const record: GhsaFixRecord = {
branch: branch ?? getSocketFixBranchName(ghsaId),
fixedAt: new Date().toISOString(),
ghsaId,
}
if (prNumber !== undefined) {
record.prNumber = prNumber
}
tracker.fixed.push(record)
// Sort by fixedAt descending, most recent first.
tracker.fixed.sort((a, b) => b.fixedAt.localeCompare(a.fixedAt))
await saveGhsaTracker(cwd, tracker)
debug(`ghsa-tracker: marked ${ghsaId} as fixed`)
} catch (e) {
debug(`ghsa-tracker: failed to mark ${ghsaId} as fixed`)
debugDir(e)
} finally {
// Release lock.
if (lockAcquired) {
await strictDelete(lockFile)
}
}
}
/**
* Save the GHSA tracker to the repository. Creates the .socket directory if it
* doesn't exist.
*/
export async function saveGhsaTracker(
cwd: string,
tracker: GhsaTracker,
): Promise<void> {
const trackerPath = path.join(cwd, TRACKER_FILE)
// Ensure .socket directory exists.
await safeMkdir(path.dirname(trackerPath), { recursive: true })
await writeJson(trackerPath, tracker, { spaces: 2 })
debug(`ghsa-tracker: saved ${tracker.fixed.length} records to ${trackerPath}`)
}