Skip to content

Commit b5badc6

Browse files
authored
Overhaul VT ID and Add gzip (#35)
1 parent 2b533e2 commit b5badc6

7 files changed

Lines changed: 102 additions & 68 deletions

File tree

.github/workflows/lint.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818

1919
permissions:
2020
pull-requests: write
21+
checks: write
2122

2223
steps:
2324
- name: "Checkout"

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ https://badges.cssnr.com/
6060
[![Uptime](https://badges.cssnr.com/uptime?style=for-the-badge)](https://badges.cssnr.com/uptime?style=for-the-badge)
6161
[![Deploy to Render](https://img.shields.io/badge/Deploy_to_Render-4351E8?style=for-the-badge&logo=render)](https://render.com/deploy?repo=https://github.com/smashedr/node-badges)
6262

63-
> [!WARNING]
63+
> [!IMPORTANT]
6464
> This is currently in beta, expect breaking changes.
6565
6666
## Badges
@@ -129,18 +129,22 @@ https://badges.cssnr.com/ghcr/tags/smashedr/node-badges?labelColor=plum&lucide=a
129129
[![VT Hash](https://badges.cssnr.com/vt/sha/sha256:d54fd9a93f2aa25b5c95128f84de1a624783ded6e66554c12a5ffd07546146e4)](https://badges.cssnr.com/vt/sha/sha256:d54fd9a93f2aa25b5c95128f84de1a624783ded6e66554c12a5ffd07546146e4)
130130
[![VT Release](https://badges.cssnr.com/vt/cssnr/zipline-android/app-release.apk)](https://badges.cssnr.com/vt/cssnr/zipline-android/app-release.apk)
131131

132-
`/vt/id/{id}`
133-
`/vt/sha/{sha}`
132+
`/vt/id/{hash}`
134133
`/vt/{owner}/{repo}/{asset}`
135134
`/vt/{owner}/{repo}/{asset}/{tag}`
136135

137-
The `id` endpoint is used for VirusTotal File ID, and `sha` for a file hash/digest.
136+
> [!WARNING]
137+
> Going forward you **need** to use the file hash: `SHA-256`, `SHA-1` or `MD5`.
138+
> File ID's (which end with `==`) consume API calls where hashes do not.
139+
> You **MUST** also update the endpoint to: `/vt/id/{hash}`
140+
> File ID's will continue to work for existing badges; however, DO NOT ADD MORE!
138141
139-
- https://badges.cssnr.com/vt/id/YjJmYTllMDdlMjFlMGUyOWEwMGVlMTM3MTM0ZGUzNGI6MTc1OTk2MDE4MQ==
140-
- https://badges.cssnr.com/vt/sha/sha256:d54fd9a93f2aa25b5c95128f84de1a624783ded6e66554c12a5ffd07546146e4
142+
- https://badges.cssnr.com/vt/id/sha256:d54fd9a93f2aa25b5c95128f84de1a624783ded6e66554c12a5ffd07546146e4
141143
- https://badges.cssnr.com/vt/cssnr/zipline-android/app-release.apk
142144
- https://badges.cssnr.com/vt/cssnr/zipline-android/app-release.apk/1.0.29
143145

146+
The `hash` is the file's `SHA-256`, `SHA-1` or `MD5`. The prefix is optional and can be `sha256:xxxx` or just `xxxx`.
147+
144148
The `owner/repo/asset` endpoints use the latest/tagged release asset for the repository.
145149

146150
If the `tag` parameter is omitted, the release tagged as `latest` in GitHub is used.
@@ -215,6 +219,8 @@ _Note: the badge at the top is also from this [docker-compose-swarm.yaml](https:
215219

216220
### Static Badge
217221

222+
[![Alt Text](https://badges.cssnr.com/static/is%20cool/node-badges)](https://badges.cssnr.com/static/is%20cool/node-badges)
223+
218224
`/static/{message}`
219225
`/static/{message}/{label}`
220226

docker-compose-dev.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ services:
44
nginx:
55
image: ghcr.io/cssnr/docker-nginx-proxy:latest
66
environment:
7-
- SERVICE_NAME=app
8-
- SERVICE_PORT=3000
7+
SERVICE_NAME: "app"
8+
SERVICE_PORT: "3000"
9+
GZIP_TYPES: "text/html image/svg+xml"
910
deploy:
1011
replicas: 1
1112
resources:

docker-compose-swarm.yaml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ services:
44
nginx:
55
image: ghcr.io/cssnr/docker-nginx-proxy:latest
66
environment:
7-
- SERVICE_NAME=app
8-
- SERVICE_PORT=3000
7+
SERVICE_NAME: "app"
8+
SERVICE_PORT: "3000"
9+
GZIP_TYPES: "text/html image/svg+xml"
910
deploy:
1011
replicas: 1
1112
resources:
@@ -39,10 +40,10 @@ services:
3940
app:
4041
image: ghcr.io/smashedr/node-badges:${VERSION:-latest}
4142
environment:
42-
- GITHUB_TOKEN=${GITHUB_TOKEN}
43-
- VT_API_KEY=${VT_API_KEY}
44-
- SENTRY_URL=${SENTRY_URL}
45-
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
43+
GITHUB_TOKEN: ${GITHUB_TOKEN}
44+
VT_API_KEY: ${VT_API_KEY}
45+
SENTRY_URL: ${SENTRY_URL}
46+
SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT}
4647
deploy:
4748
replicas: 1
4849
resources:

docker-compose.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ services:
44
nginx:
55
image: ghcr.io/cssnr/docker-nginx-proxy:latest
66
environment:
7-
- SERVICE_NAME=app
8-
- SERVICE_PORT=3000
7+
SERVICE_NAME: "app"
8+
SERVICE_PORT: "3000"
9+
GZIP_TYPES: "text/html image/svg+xml"
910
deploy:
1011
replicas: 1
1112
resources:

src/api.js

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,14 @@ export class GhcrApi {
114114

115115
/**
116116
* Get VirusTotal Stats for a Release Asset
117-
* @param {Request} req
117+
* @param {import('express').Request} req
118118
* @return {Promise<Object>}
119119
*/
120120
export async function getVTReleaseStats(req) {
121121
const tag = req.params.tag || 'latest'
122122
const key = `${req.params.owner}/${req.params.repo}/${req.params.asset}/${tag}`
123123
console.log('key:', key)
124-
// NOTE: Consider making this block a reusable function similar to cacheError
124+
// NOTE: Duplicate Code - 5 lines
125125
const cached = await cacheGet(key)
126126
if (cached) {
127127
if (cached.errorMessage) throw new Error(cached.errorMessage)
@@ -143,29 +143,24 @@ export async function getVTReleaseStats(req) {
143143
if (!asset) await cacheError(key, 'Asset Not Found')
144144
console.log('asset?.digest:', asset?.digest)
145145
if (!asset?.digest) await cacheError(key, 'Digest Not Found')
146-
const sha = asset.digest.split(':')[1]
147-
console.log('sha:', sha)
148-
// const vt = new VTApi(process.env.VT_API_KEY)
149-
// const report = await vt.getReport(sha)
150-
// console.log('report:', report)
151-
// if (!report) await cacheError(key, 'VT Report Not Found')
152-
const stats = await getVTStats(sha)
146+
const hash = asset.digest.split(':')[1]
147+
console.log('hash:', hash)
148+
const stats = await getVTStats(hash)
153149
console.log('last_analysis_stats:', stats)
154150
if (!stats) await cacheError(key, 'VT Stats Not Found')
155151
await cacheSet(key, stats)
156152
return stats
157153
}
158154

159155
/**
160-
* Get VT Stats for a File ID/SHA
161-
* @param {String} sha
162-
* @param {Boolean} [id]
156+
* Get VT Stats for a File ID/Hash
157+
* @param {String} hash
163158
* @return {Promise<Object>}
164159
*/
165-
export async function getVTStats(sha, id = false) {
166-
const key = `/vt/${id ? 'id' : 'sha'}/${sha}`
160+
export async function getVTStats(hash) {
161+
const key = `/vt/id/${hash}`
167162
console.log('key:', key)
168-
// NOTE: Consider making this block a reusable function similar to cacheError
163+
// NOTE: Duplicate Code - 5 lines
169164
const cached = await cacheGet(key)
170165
if (cached) {
171166
if (cached.errorMessage) throw new Error(cached.errorMessage)
@@ -174,29 +169,27 @@ export async function getVTStats(sha, id = false) {
174169
console.log(`-- CACHE MISS: ${key}`)
175170
const vt = new VTApi(process.env.VT_API_KEY)
176171
let stats
177-
if (id) {
178-
console.log('getAnalysis')
179-
const data = await vt.getAnalysis(sha)
172+
if (hash.endsWith('==')) {
173+
console.log('getAnalysis - DEPRECATED') // TODO: Deprecated
174+
const data = await vt.getAnalysis(hash)
180175
// console.log('data:', JSON.stringify(data, null, 2))
176+
// noinspection JSUnresolvedReference
181177
stats = data?.data?.attributes?.stats
182178
} else {
183179
console.log('getReport')
184-
const data = await vt.getReport(sha)
180+
const data = await vt.getReport(hash)
185181
// console.log('data:', JSON.stringify(data, null, 2))
182+
// noinspection JSUnresolvedReference
186183
stats = data?.data?.attributes?.last_analysis_stats
187184
}
188-
// console.log('report:', report)
189-
// if (!stats) await cacheError(key, 'VT Report Not Found')
190-
// const stats = report?.data?.attributes?.last_analysis_stats
191-
// console.log('stats:', stats)
192185
if (!stats) await cacheError(key, 'VT Stats Not Found')
193-
await cacheSet(key, stats, 60 * 60 * 24)
186+
await cacheSet(key, stats, 60 * 60 * 48)
194187
return stats
195188
}
196189

197190
/**
198191
* Get JSONPath for JSON/YAML
199-
* @param {Request} req
192+
* @param {import('express').Request} req
200193
* @return {Promise<String>}
201194
*/
202195
export async function getJSONPath(req) {

src/app.js

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,28 @@ const app = express()
2727
const port = process.env.PORT || 3000
2828

2929
app.use(express.static('src/public'))
30-
app.use(express.json())
3130
app.use(cors({ methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'PURGE'] }))
3231

3332
app.set('views', 'src/views')
3433
app.set('view engine', 'pug')
3534
app.disable('view cache')
35+
app.disable('x-powered-by')
3636

3737
console.log(`APP_VERSION: ${process.env.APP_VERSION}`)
3838
console.log(`GITHUB_TOKEN: ${process.env.GITHUB_TOKEN ? 'Loaded' : 'MISSING'}`)
3939
console.log(`VT_API_KEY: ${process.env.VT_API_KEY ? 'Loaded' : 'MISSING'}`)
4040

41-
app.listen(port, () => {
41+
const server = app.listen(port, () => {
4242
console.log(`Listening on PORT: ${port}`)
4343
})
4444

45+
process.on('SIGTERM', () => {
46+
console.log('SIGTERM signal received: closing HTTP server')
47+
server.close(() => {
48+
console.log('HTTP server closed')
49+
})
50+
})
51+
4552
app.get('/app-health-check', (req, res) => {
4653
res.sendStatus(200)
4754
})
@@ -93,11 +100,11 @@ app.all('/vt/:type/:hash', async (req, res, next) => {
93100
if (req.method === 'PURGE') {
94101
console.log('PURGE:', req.originalUrl)
95102
if (!['id', 'sha'].includes(req.params.type)) return next()
96-
let hash = req.params.hash
97-
if (req.params.hash === 'sha') {
98-
hash = hash.includes(':') ? hash.split(':')[1] : hash
99-
}
100-
const key = `/vt/${req.params.type === 'id' ? 'id' : 'sha'}/${hash}`
103+
const hash = req.params.hash.includes(':')
104+
? req.params.hash.split(':')[1]
105+
: req.params.hash
106+
console.log('hash:', hash)
107+
const key = `/vt/id/${hash}`
101108
return purgeKey(res, key)
102109
}
103110
next()
@@ -109,13 +116,14 @@ app.get(
109116
console.log(req.originalUrl)
110117
// console.log('req.params.type:', req.params.type)
111118
if (!['id', 'sha'].includes(req.params.type)) return res.sendStatus(404)
112-
113119
if (!process.env.VT_API_KEY) throw new Error('Missing VT API Key')
114-
let hash = req.params.hash
115-
if (req.params.type === 'sha') {
116-
hash = hash.includes(':') ? hash.split(':')[1] : hash
117-
}
118-
const stats = await getVTStats(hash, req.params.type === 'id')
120+
121+
const hash = req.params.hash.includes(':')
122+
? req.params.hash.split(':')[1]
123+
: req.params.hash
124+
console.log('hash:', hash)
125+
126+
const stats = await getVTStats(hash)
119127
// console.log('stats:', stats)
120128
const message = `${stats.malicious}/${stats.suspicious}/${stats.undetected}`
121129
console.log('message:', message)
@@ -277,6 +285,20 @@ app.get(
277285
})
278286
)
279287

288+
// Handler 404
289+
app.use((req, res) => {
290+
// res.status(404).send("Sorry can't find that!")
291+
const data = {
292+
message: '404 - URL Not Found',
293+
color: 'red',
294+
style: req.query.style || 'flat',
295+
}
296+
console.log('data:', data)
297+
// noinspection JSCheckFunctionSignatures
298+
const badge = makeBadge(data)
299+
sendBadge(res, badge, 404)
300+
})
301+
280302
if (Sentry) Sentry.setupExpressErrorHandler(app)
281303

282304
function errorBadgeHandler(handler) {
@@ -298,12 +320,32 @@ function errorBadgeHandler(handler) {
298320
}
299321
}
300322

323+
/**
324+
* Set SVG Headers
325+
* @param {express.Response} res
326+
*/
327+
function setHeaders(res) {
328+
res.setHeader('Content-Type', 'image/svg+xml')
329+
res.setHeader('Cache-Control', 'public, max-age=3600')
330+
}
331+
332+
/**
333+
* Send Badge
334+
* @param {express.Response} res
335+
* @param {String} badge
336+
* @param {Number} [status]
337+
*/
338+
function sendBadge(res, badge, status = 200) {
339+
setHeaders(res)
340+
res.status(status).send(badge)
341+
}
342+
301343
/**
302344
* Get Badge
303345
* @param {String} message Badge Message
304346
* @param {Object} [query] req.query Object
305347
* @param {Object} [options] Badge Options
306-
* @param {Response} [res] To also sendBadge
348+
* @param {express.Response} [res] To sendBadge
307349
* @return {String}
308350
*/
309351
function getBadge(message, query = {}, options = {}, res = null) {
@@ -327,17 +369,6 @@ function getBadge(message, query = {}, options = {}, res = null) {
327369
return badge
328370
}
329371

330-
/**
331-
* Send Badge
332-
* @param {Response} res
333-
* @param {String} badge
334-
*/
335-
function sendBadge(res, badge) {
336-
res.setHeader('Content-Type', 'image/svg+xml')
337-
res.setHeader('Cache-Control', 'public, max-age=3600')
338-
res.send(badge)
339-
}
340-
341372
/**
342373
* Get Logo String
343374
* @param {Object} query
@@ -380,7 +411,7 @@ function getLogo(query, opts, color = '#fff') {
380411

381412
/**
382413
* Purge Key Response
383-
* @param {Response} res
414+
* @param {express.Response} res
384415
* @param {String} key
385416
* @return {Promise<void>}
386417
*/
@@ -420,7 +451,7 @@ function getUptime() {
420451

421452
/**
422453
* Get Ranged Color w/ Options
423-
* @param {Request} req
454+
* @param {express.Request} req
424455
* @param {Number} index
425456
* @param {Object} [options]
426457
* @return {String}

0 commit comments

Comments
 (0)