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
2 changes: 1 addition & 1 deletion apps/docs/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const config = {
],
},
output: "export",
trailingSlash: true
trailingSlash: true,
}

export default withMDX(config)
7 changes: 7 additions & 0 deletions packages/resafe/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [1.0.3](https://github.com/ofabiodev/resafe/compare/v1.0.2...v1.0.3) (2025-12-27)


### Bug Fixes

* unify log formatting with customizable colors and properties ([#11](https://github.com/ofabiodev/resafe/issues/11)) ([7cfb28f](https://github.com/ofabiodev/resafe/commit/7cfb28f1420faef127429aa568ed0d03850e315a))

## [1.0.2](https://github.com/ofabiodev/resafe/compare/v1.0.1...v1.0.2) (2025-12-26)


Expand Down
2 changes: 1 addition & 1 deletion packages/resafe/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "resafe",
"version": "1.0.2",
"version": "1.0.3",
"description": "🛡️ Detects ReDoS vulnerabilities in regexes using Thompson NFA construction and spectral radius analysis",
"license": "MIT",
"homepage": "https://resafe.js.org",
Expand Down
2 changes: 1 addition & 1 deletion packages/resafe/src/core/spectral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export function spectralRadius(matrix: number[][]): number {
converged = false
break
}
}
}
if (converged) break
v = normalizedV
}
Expand Down
74 changes: 60 additions & 14 deletions packages/resafe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,69 @@ export interface Options extends Config {
throwErr?: boolean
}

export function check(regex: string | RegExp, options: Options = {}): Result {
export function check(
regex: string | RegExp,
options: Options = {},
): Result | null {
if (regex == null || regex === "") {
const err = new Error("Empty regex. Provide a valid pattern.")
log.warn("Empty regex!", {
lines: ["? Provide a valid regex."],
})
if (options.throwErr) throw err
return null
}

if (typeof regex !== "string" && !(regex instanceof RegExp)) {
const err = new TypeError("Regex must be string or RegExp")
log.error("Invalid regex!", {
property: { name: "type", value: typeof regex },
})
throw err
}

const pattern = typeof regex === "string" ? regex : regex.source
const result = analyze(pattern, options)
const radius = Number(result.radius.toFixed(4))

if (!result.safe && !options.silent) {
log.error("Unsafe Regex!", {
property: { name: "regex", value: `/${pattern}/`, color: (f) => f.red() },
lines: [
`Spectral radius: ${radius} (threshold: ${options.threshold ?? 1.0})`,
"? Consider simplifying quantifiers",
]
try {
new RegExp(pattern)
} catch (err) {
if (err instanceof SyntaxError) {
log.error("Invalid regex syntax!", {
lines: [`? ${err.message}`],
})
if (options.throwErr) throw err
return null
}
throw err
}

if (/\\u\{[0-9A-Fa-f]+\}/.test(pattern)) {
log.warn("Unicode escape sequences may not be fully supported", {
property: { name: "pattern", value: pattern },
})
}

if (!result.safe && options.throwErr) {
throw new Error(`Unsafe regex (spectral radius ${radius})`)
const result = analyze(pattern, options)
const radius = Number(result.radius.toFixed(4))

if (!result.safe) {
const err = new Error(`Unsafe regex (spectral radius ${radius})`)

if (!options.silent) {
log.error("Unsafe Regex!", {
property: {
name: "regex",
value: `/${pattern}/`,
color: (f) => f.red(),
},
lines: [
`Spectral radius: ${radius} (threshold: ${options.threshold ?? 1.0})`,
"? Consider simplifying quantifiers",
],
})
}

if (options.throwErr) throw err
}

return result
Expand All @@ -31,8 +77,8 @@ export function check(regex: string | RegExp, options: Options = {}): Result {
export async function checkAsync(
regex: string | RegExp,
options: Options = {},
): Promise<Result> {
): Promise<Result | null> {
return Promise.resolve(check(regex, options))
}

export type { Result, Config } from "./core/analyzer.ts"
export type { Result, Config } from "./core/analyzer.ts"
2 changes: 1 addition & 1 deletion packages/resafe/src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,4 @@ export const log = {
formatLogLine((f) => f.pastelRedBg(), msg, options),
warn: (msg: string, options?: LogOptions) =>
formatLogLine((f) => f.pastelYellowBg(), msg, options),
}
}