|
|
|
const fs = require('fs')
|
|
|
|
|
|
|
|
const iconPath = './src/images/icons'
|
|
|
|
|
|
|
|
const categories = JSON.parse(fs.readFileSync('./src/categories.json'))
|
|
|
|
|
|
|
|
let technologies = {}
|
|
|
|
|
|
|
|
for (const index of Array(27).keys()) {
|
|
|
|
const charCode = index ? index + 96 : 95
|
|
|
|
const character = String.fromCharCode(charCode)
|
|
|
|
|
|
|
|
const _technologies = JSON.parse(
|
|
|
|
fs.readFileSync(`./src/technologies/${character}.json`)
|
|
|
|
)
|
|
|
|
|
|
|
|
Object.keys(_technologies).forEach((name) => {
|
|
|
|
const _charCode = name.toLowerCase().charCodeAt(0)
|
|
|
|
|
|
|
|
if (charCode !== _charCode) {
|
|
|
|
if (_charCode < 97 || _charCode > 122) {
|
|
|
|
if (charCode !== 95) {
|
|
|
|
throw new Error(
|
|
|
|
`${name} should be moved from ./src/technologies/${character}.json to ./src/technologies/_.json`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
`${name} should be moved from ./src/technologies/${character}.json to ./src/technologies/${String.fromCharCode(
|
|
|
|
_charCode
|
|
|
|
)}.json`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
technologies = {
|
|
|
|
...technologies,
|
|
|
|
..._technologies,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys(technologies).forEach((name) => {
|
|
|
|
const technology = technologies[name]
|
|
|
|
|
|
|
|
// Validate regular expressions
|
|
|
|
;['url', 'html', 'meta', 'headers', 'cookies', 'script', 'js'].forEach(
|
|
|
|
(type) => {
|
|
|
|
if (technology[type]) {
|
|
|
|
const keyed =
|
|
|
|
typeof technology[type] === 'string' ||
|
|
|
|
Array.isArray(technology[type])
|
|
|
|
? { _: technology[type] }
|
|
|
|
: technology[type]
|
|
|
|
|
|
|
|
Object.keys(keyed).forEach((key) => {
|
|
|
|
const patterns = Array.isArray(keyed[key]) ? keyed[key] : [keyed[key]]
|
|
|
|
|
|
|
|
patterns.forEach((pattern, index) => {
|
|
|
|
const id = `${name}: ${type}[${key === '_' ? `${index}` : key}]`
|
|
|
|
|
|
|
|
const [regex, ...flags] = pattern.split('\\;')
|
|
|
|
|
|
|
|
let maxGroups = 0
|
|
|
|
|
|
|
|
flags.forEach((flag) => {
|
|
|
|
const [key, value] = flag.split(':')
|
|
|
|
|
|
|
|
if (key === 'version') {
|
|
|
|
const refs = value.match(/\\(\d+)/g)
|
|
|
|
|
|
|
|
if (refs) {
|
|
|
|
maxGroups = refs.reduce((max, ref) =>
|
|
|
|
Math.max(max, parseInt(refs[1] || 0))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else if (key === 'confidence') {
|
|
|
|
if (
|
|
|
|
!/^\d+$/.test(value) ||
|
|
|
|
parseInt(value, 10) < 0 ||
|
|
|
|
parseInt(value, 10) > 99
|
|
|
|
) {
|
|
|
|
throw new Error(
|
|
|
|
`Confidence value must a number between 0 and 99: ${value} (${id})`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(`Invalid flag: ${key} (${id})`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Validate regular expression
|
|
|
|
try {
|
|
|
|
// eslint-disable-next-line no-new
|
|
|
|
new RegExp(regex)
|
|
|
|
} catch (error) {
|
|
|
|
throw new Error(`${error.message} (${id})`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Count capture groups
|
|
|
|
const groups = new RegExp(`${regex}|`).exec('').length - 1
|
|
|
|
|
|
|
|
if (groups > maxGroups) {
|
|
|
|
throw new Error(
|
|
|
|
`Too many non-capturing groups, expected ${maxGroups}: ${regex} (${id})`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === 'html' && !/[<>]/.test(regex)) {
|
|
|
|
throw new Error(
|
|
|
|
`HTML pattern must include < or >: ${regex} (${id})`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
// Validate categories
|
|
|
|
technology.cats.forEach((id) => {
|
|
|
|
if (!categories[id]) {
|
|
|
|
throw new Error(`No such category: ${id} (${name})`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Validate icons
|
|
|
|
if (technology.icon && !fs.existsSync(`${iconPath}/${technology.icon}`)) {
|
|
|
|
throw new Error(`No such icon: ${technology.icon} (${name})`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate website URLs
|
|
|
|
try {
|
|
|
|
// eslint-disable-next-line no-new
|
|
|
|
const { protocol } = new URL(technology.website)
|
|
|
|
|
|
|
|
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
|
|
throw new Error('Invalid protocol')
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
throw new Error(`Invalid website URL: ${technology.website} (${name})`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate implies and excludes
|
|
|
|
const { implies, excludes } = technology
|
|
|
|
|
|
|
|
if (implies) {
|
|
|
|
;(Array.isArray(implies) ? implies : [implies]).forEach((implied) => {
|
|
|
|
const [_name, ...flags] = implied.split('\\;')
|
|
|
|
|
|
|
|
const id = `${name}: implies[${implied}]`
|
|
|
|
|
|
|
|
if (!technologies[_name]) {
|
|
|
|
throw new Error(`Implied technology does not exist: ${_name} (${id})`)
|
|
|
|
}
|
|
|
|
|
|
|
|
flags.forEach((flag) => {
|
|
|
|
const [key, value] = flag.split(':')
|
|
|
|
|
|
|
|
if (key === 'version') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (key === 'confidence') {
|
|
|
|
if (
|
|
|
|
!/^\d+$/.test(value) ||
|
|
|
|
parseInt(value, 10) < 0 ||
|
|
|
|
parseInt(value, 10) > 99
|
|
|
|
) {
|
|
|
|
throw new Error(
|
|
|
|
`Confidence value must a number between 0 and 99: ${value} (${id})`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(`Invalid flag: ${key} (${id})`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if (excludes) {
|
|
|
|
;(Array.isArray(excludes) ? excludes : [excludes]).forEach((excluded) => {
|
|
|
|
const id = `${name}: excludes[${excluded}]`
|
|
|
|
|
|
|
|
if (!technologies[excluded]) {
|
|
|
|
throw new Error(
|
|
|
|
`Excluded technology does not exist: ${excluded} (${id})`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Validate icons
|
|
|
|
fs.readdirSync(iconPath).forEach((file) => {
|
|
|
|
const filePath = `${iconPath}/${file}`
|
|
|
|
|
|
|
|
if (fs.statSync(filePath).isFile() && !file.startsWith('.')) {
|
|
|
|
if (!/^(png|svg)$/i.test(file.split('.').pop())) {
|
|
|
|
throw new Error(`Incorrect file type, expected PNG or SVG: ${filePath}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!Object.values(technologies).some(({ icon }) => icon === file) &&
|
|
|
|
file !== 'default.svg'
|
|
|
|
) {
|
|
|
|
throw new Error(`Extraneous file: ${filePath}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|