import fs from 'node:fs/promises' import path from 'node:path' import logger from './logger' export const SNIPPET_PATHS = Object.freeze(['./server.ts', './routes', './lib', './data', './data/static/web3-snippets', './frontend/src/app', './models']) interface FileMatch { path: string content: string } interface CachedCodeChallenge { snippet: string vulnLines: number[] neutralLines: number[] } export const findFilesWithCodeChallenges = async (paths: readonly string[]): Promise => { const matches = [] for (const currPath of paths) { if ((await fs.lstat(currPath)).isDirectory()) { const files = await fs.readdir(currPath) const moreMatches = await findFilesWithCodeChallenges( files.map(file => path.resolve(currPath, file)) ) matches.push(...moreMatches) } else { try { const code = await fs.readFile(currPath, 'utf8') if ( // strings are split so that it doesn't find itself... code.includes('// vuln-code' + '-snippet start') || code.includes('# vuln-code' + '-snippet start') ) { matches.push({ path: currPath, content: code }) } } catch (e) { logger.warn(`File ${currPath} could not be read. it might have been moved or deleted. If coding challenges are contained in the file, they will not be available.`) } } } return matches } function getCodeChallengesFromFile (file: FileMatch) { const fileContent = file.content // get all challenges which are in the file by a regex capture group const challengeKeyRegex = /[/#]{0,2} vuln-code-snippet start (?.*)/g const challenges = [...fileContent.matchAll(challengeKeyRegex)] .flatMap(match => match.groups?.challenges?.split(' ') ?? []) .filter(Boolean) return challenges.map((challengeKey) => getCodingChallengeFromFileContent(fileContent, challengeKey)) } function getCodingChallengeFromFileContent (source: string, challengeKey: string) { const snippets = source.match(`[/#]{0,2} vuln-code-snippet start.*${challengeKey}([^])*vuln-code-snippet end.*${challengeKey}`) if (snippets == null) { throw new BrokenBoundary('Broken code snippet boundaries for: ' + challengeKey) } let snippet = snippets[0] // TODO Currently only a single code snippet is supported snippet = snippet.replace(/\s?[/#]{0,2} vuln-code-snippet start.*[\r\n]{0,2}/g, '') snippet = snippet.replace(/\s?[/#]{0,2} vuln-code-snippet end.*/g, '') snippet = snippet.replace(/.*[/#]{0,2} vuln-code-snippet hide-line[\r\n]{0,2}/g, '') snippet = snippet.replace(/.*[/#]{0,2} vuln-code-snippet hide-start([^])*[/#]{0,2} vuln-code-snippet hide-end[\r\n]{0,2}/g, '') snippet = snippet.trim() let lines = snippet.split('\r\n') if (lines.length === 1) lines = snippet.split('\n') if (lines.length === 1) lines = snippet.split('\r') const vulnLines = [] const neutralLines = [] for (let i = 0; i < lines.length; i++) { if (new RegExp(`vuln-code-snippet vuln-line.*${challengeKey}`).exec(lines[i]) != null) { vulnLines.push(i + 1) } else if (new RegExp(`vuln-code-snippet neutral-line.*${challengeKey}`).exec(lines[i]) != null) { neutralLines.push(i + 1) } } snippet = snippet.replace(/\s?[/#]{0,2} vuln-code-snippet vuln-line.*/g, '') snippet = snippet.replace(/\s?[/#]{0,2} vuln-code-snippet neutral-line.*/g, '') return { challengeKey, snippet, vulnLines, neutralLines } } class BrokenBoundary extends Error { constructor (message: string) { super(message) this.name = 'BrokenBoundary' this.message = message } } // dont use directly, use getCodeChallenges getter let _internalCodeChallenges: Map | null = null export async function getCodeChallenges (): Promise> { if (_internalCodeChallenges === null) { _internalCodeChallenges = new Map() const filesWithCodeChallenges = await findFilesWithCodeChallenges(SNIPPET_PATHS) for (const fileMatch of filesWithCodeChallenges) { for (const codeChallenge of getCodeChallengesFromFile(fileMatch)) { _internalCodeChallenges.set(codeChallenge.challengeKey, { snippet: codeChallenge.snippet, vulnLines: codeChallenge.vulnLines, neutralLines: codeChallenge.neutralLines }) } } } return _internalCodeChallenges }