antivirus_service
One-liner
A small Node.js RPC microservice that receives a file over RabbitMQ, scans it with the local Avast daemon (via a Unix domain socket), and returns a structured “is it safe?” verdict. A separate cron process keeps the Avast virus definitions (VPS) up to date. Verified from
README.md:1-5andpackage.json:1-3.
- Repo:
/Users/levander/coding/facekom-v2-clones/antivirus_service - Base branch:
devel(only branch present;git branch -a→devel,remotes/origin/devel) - HEAD at clone:
9c6d7d5— “Bump braces from 3.0.2 to 3.0.3 (#9)” by dependabot, 2024-09-24 (git log -1) - Size: 29 git-tracked files (
git ls-files | wc -l); nonode_modules, no Dockerfile, notest/dir, no Git-LFS / binary blobs (git ls-filescontains no.bin/.so/.node/.tar/.model/...— verified). - Upstream:
git+https://github.com/TechTeamer/antivirus_service.git(package.json:18)
See also: architecture-overview, customization-architecture, customization-branch-catalog.
1. Purpose & role in FaceKom
A generic antivirus scanner service (package.json:3 description “Generic antivirus scanner service.”). It is an RPC server sitting on a RabbitMQ queue. Other FaceKom services act as RPC clients: they send a message + file attachment and synchronously receive a scan verdict.
From README.md:3-35:
- Request message (JSON body):
{ "fileName": "name of the File", "id": "reference ID for debuging" } - Request attachment: a
Mapwith keyfile→ fileBuffer(attachment.set('file', fileBuffer)). - Response (result of Avast scan):
{ "is_safe": true, "is_infected": false, "is_excluded": false, "is_password_protected": false, "malware_names": [] }
The response object’s exact shape is produced by the @techteamer/avastscan dependency’s scanFile() (not implemented in this repo — see Unverified/gaps). This repo only orchestrates: queue ↔ temp-file ↔ Avast socket.
Two independent OS processes ship together (both run under supervisord, see §3):
server.js— the RPC scanner server.cron.js— periodic Avast VPS (Virus Protection Stream / definitions) updater.
2. Tech stack & runtime
| Aspect | Value | Source |
|---|---|---|
| Language | JavaScript (CommonJS, require) | all *.js; .eslintrc.json:20 "commonjs": true |
| Runtime | Node.js >= 16.14.2 | package.json:13-15 "engines": { "node": ">=16.14.2" } |
| CI Node | Node 16 on Ubuntu focal | .travis.yml:6-7 |
| Package manager | Yarn (frozen lockfile; yarn.lock present, 84.5 KB) | scripts/travis.sh:6, yarn.lock |
| Messaging lib | @techteamer/mq ^6.3.1 (RabbitMQ RPC/ConnectionPool) | package.json:31 |
| AV integration | @techteamer/avastscan ^2.1.0 | package.json:30 |
| Cron scheduler | cronosjs ^1.7.1 | package.json:32 |
| Config loader | getconfig ^4.5.0 (NODE_ENV / RUNTIME_ENV based) | package.json:33, config/config.js:6 |
| Logging | log4js ^6.5.2 | package.json:35, logger.js |
| Temp files | tmp-promise ^3.0.3 | package.json:36, avast/AvastService.js:3 |
| License report | license-checker ^25.0.1 | package.json:34, bin/credits |
| Lint | ESLint ^8 + eslint-config-standard ^16 (+ node/import/promise/jest plugins) | package.json:22-27 |
"private": true | not published to npm | package.json:20 |
No build step / no transpile
Plain CommonJS executed directly by Node. There is no TypeScript, no bundler, no compile. “Build” =
yarn install+ supervisord startingnode server.js/node cron.js.
3. Build & run
package.json scripts (verbatim, package.json:8-12)
| Script | Command | What it does |
|---|---|---|
lint | eslint . | Lints the whole repo with the root .eslintrc.json (standard + node/recommended). |
test | npm run lint --silent && echo 'npm test: OK' | The “test suite” is lint-only. Runs lint; if it passes, prints npm test: OK. There are no unit tests in the repo. |
credits | node bin/credits > CREDITS.html | Generates an HTML credits/licenses table of all dependencies into CREDITS.html (git-ignored, .gitignore:6). |
Entrypoints
server.js(RPC server process). Boot sequence (server.js:1-33):bootstrap/process-settings()thenbootstrap/process-listeners(true)(true= graceful 1 s SIGTERM/SIGINT delay).- Build
ConnectionPoolfrom@techteamer/mqwithdefaultConnectionName: 'default'. setupQueueManagers(config.get('queue')), get the default connection.queue.getRPCServer(config.get('server.queueName'), AntiVirusScannerRPCServer)— binds the server class to the queue nameanti-virus-scanner.- On RabbitMQ
connection 'close'→ log +process.exit(2)(relies on supervisord restart). connectionPool.connect().
cron.js(definitions-updater process,cron.js:1-11): bootstraps, thennew AvastVPSUpdateCronJob(config.get('cron'), logger).start().bin/credits(#!/usr/bin/env node,bin/credits:1) — CLI helper, not a service. See §5.
There is no "main" or "bin" field in package.json; processes are launched explicitly via supervisord.
Dockerfile
No Dockerfile in this repo
ls Dockerfile* docker-compose*→ no matches. The basedevelbranch ships no container build. Runtime is expected to be a host/VM (likely customization branches add packaging — see customization-branch-catalog). The supervisor configs referenceRUNTIME_ENV=dockerand paths under/workspace/..., implying a Docker image is built elsewhere.
How it actually runs — supervisord
Both supervisor_dev.conf and supervisor_docker.conf define three programs (identical except environment=RUNTIME_ENV=dev vs =docker):
| program | command | notes |
|---|---|---|
antivirus_service | node server.js | autostart/autorestart, exitcodes=0,2, stopsignal=TERM, log → /var/log/antivirus_service.log |
avast_vps_update_cron | node cron.js | same restart policy, log → /var/log/avast_vps_update_cron.log |
avast | /workspace/antivirus_service/avast.sh | runs the Avast daemon; stopasgroup=true; log → /var/log/avast.log |
(supervisor_dev.conf:1-57, supervisor_docker.conf:1-57.)
avast.sh (avast.sh:1-7) — the Avast daemon launcher:
/usr/libexec/avast/vpsupdate # update virus defs once on start
echo "VPS update finished"
/usr/bin/avast -vv # start the avast daemon (verbose), provides the scan.sockStart command summary
Production/dev start is not
npm start(no such script). It is supervisord launchingnode server.js,node cron.js, andavast.shas three managed processes. The Node services talk to the Avast process over the Unix socket it exposes (/run/avast/scan.sock).
4. Top-level structure
Verified by ls of each dir + reading every file. (29 tracked files total.)
| Path | What it is |
|---|---|
server.js | RPC server process entrypoint (queue → scanner). |
cron.js | Cron process entrypoint (Avast VPS auto-update). |
logger.js | log4js singleton; picks channel name from argv[1] (server.js→antivirus-scanner-service, cron.js→avast-vps-update). |
avast.sh | Shell launcher for the Avast daemon (vpsupdate then avast -vv). |
avast/AvastService.js | Wraps @techteamer/avastscan Avast; writes attachment to temp file and scans it. |
rpc_server/AntiVirusScannerRPCServer.js | RPC handler: validates message, pulls file attachment, invokes AvastService. |
rpc_client/AntiVirusScannerRPCClient.js | Client SDK (scanFile(fileName, id, fileBuffer, timeoutMs)) for other services to call this one. |
cron/AvastVPSUpdateCronJob.js | cronosjs job; on schedule runs sh /usr/libexec/avast/vpsupdate. |
bootstrap/process-settings.js | Sets TZ=Etc/UTC; optional self-signed-cert TLS bypass. |
bootstrap/process-listeners.js | Global uncaughtException/unhandledRejection/exit/signal/warning handlers. |
config/config.js | getconfig wrapper adding .get/.has/.clone; pretty-prints JSON syntax errors; travisci override. |
config/default.json | Default config (prod-ish: 3-node amqps cluster placeholders, quorum queues). |
config/dev.json | Dev config (same as default but vhost box instead of <vhost>). |
bin/credits | Node CLI generating CREDITS.html license report. |
bin/.eslintrc.json | ESLint override for bin/ (allows console). |
scripts/travis.sh | CI script: install, yarn audit, npm test, git-clean check, executable-location check. |
tmp/.gitkeep | Keeps the temp dir (scan temp files written here per config). |
supervisor_dev.conf / supervisor_docker.conf | supervisord process definitions (3 programs each). |
.travis.yml | Travis CI pipeline (Node 16 + SonarCloud scan stage). |
sonar-project.properties | SonarCloud project key TechTeamer_antivirus_service, org techteamer. |
build.ignore | Files excluded from production runtime build (dev config, tests, eslint, docs…). |
.eslintrc.json / .eslintignore / .editorconfig / .gitignore | Lint & editor & VCS config. |
yarn.lock | Yarn lockfile (dependency pin). |
README.md | Usage, RPC message shape, config reference. |
There is no src/ — code lives in topic dirs at the repo root.
5. First-party bin/ scripts
Only one first-party bin/ directory exists: ./bin/. (scripts/travis.sh is in scripts/, covered in §8.)
bin/credits(bin/credits:1-27,#!/usr/bin/env node):- Bootstraps process settings + listeners.
- Runs
license-checker’sinit({ start: process.cwd(), production: false })(promisified). - Emits an HTML
<table>listing every dependency’s Name / License / repo URL / Publisher to stdout. - Invoked by the
creditsnpm script which redirects output toCREDITS.html.
bin/.eslintrc.json(bin/.eslintrc.json:1-8): not a script — an ESLint override turningno-consoleoff forbin/and settingenv.node: true(becausebin/creditslegitimately usesconsole.log).
bin/is whitelisted for executablesCI’s
scripts/travis.sh:21lists^./bin/inIGNORED_EXECUTABLES, so files underbin/are allowed to have the executable bit (the check rejects stray executables elsewhere).
6. Key modules / services / entities
rpc_server/AntiVirusScannerRPCServer.js (:1-43)
Extends RPCServer from @techteamer/mq. _callback(message, request):
- Destructures
{ fileName, id }from the message body. - If either is missing → returns
{ error: true, message: 'Missing file metadata...' }(does not throw). request.getAttachment('file'); if absent → returns{ error: true, message: 'Did not recieve any attachment...' }.- Reads
config.get('avast'), buildsnew AvastService(avastConfig.sockFile, avastConfig.timeout, this.logger), and returnsscanner.scanAttachment(fileName, attachment).
A new
AvastService/socket connection is created per requestThe server instantiates
AvastServiceinside_callbackon every message (:36), so there is no pooled/long-lived Avast socket connection. Combined withprefetchCount: 1(§7), throughput is one file at a time per connection.
avast/AvastService.js (:1-42)
Extends Avast from @techteamer/avastscan. Default ctor args: sockFile='/var/run/avast/scan.sock', timeoutMs=30000.
scanAttachment(fileName, attachmentBuffer): creates a temp file from the buffer,await this.scanFile(tempFile.path)(inherited fromAvast), and alwaystempFile.cleanup()infinally. On error logs and throwsError('Error during scanning the following file: ${fileName}.')._createTempFile(buffer, options):tmp.file(options)thenfs.writeFile.tmp.setGracefulCleanup()is set at module load (:4) so temp files are cleaned on process exit too.
tempFile.cleanup()can throw if temp creation failedIf
_createTempFilethrows,tempFilestaysundefinedand thefinallyblock callstempFile.cleanup()(:22) → aTypeErrorwould mask the original error. Verified atavast/AvastService.js:14-23.
rpc_client/AntiVirusScannerRPCClient.js (:1-19)
Extends RPCClient. scanFile(fileName, id, fileBuffer, timeoutMs = 30000) builds a Map with file → buffer and calls this.call({ fileName, id }, timeoutMs, attachment). This is the SDK other FaceKom services import to talk to this service (the client is exported but never used inside this repo).
cron/AvastVPSUpdateCronJob.js (:1-88)
cronosjs-based scheduler.
start(): validatesthis.scheduleviacronosjs.validate; schedules a task with{ missingHour: 'insert', skipRepeatedHour: true, timezone }. Tracksstatus(stopped/scheduled/running).run():await exec('sh /usr/libexec/avast/vpsupdate')(promisifiedchild_process.exec), logs stdout; logs stderr as error; catches and logs failures.stop()/destroy()/reload()lifecycle helpers.
Two real bugs in the cron job
run()’s catch handler logsthis.name(:27) butnameis never set on the instance → alwaysundefined.stop()anddestroy()callthis.logger.logger.info(...)(:69,:75) — double.logger. Sinceloggeris the log4js logger (no.loggerproperty), callingstop()/destroy()/reload()would throwTypeError. The cron’s normal scheduled path (start/run) is unaffected.
logger.js (:1-33)
log4js with a single colored stdout appender, level debug. Channel name derived from process.argv[1]: ends with server.js → antivirus-scanner-service; cron.js → avast-vps-update; else unknown. Exposes logger.shutdown = () => log4js.shutdown().
config/config.js (:1-91)
Wraps getconfig. Notable behaviors:
- Catches
getconfigSyntaxError, parses the bad file path + offset from the message, prints the offending ~40-char JSON slice toconsole.error, andprocess.exit(2)(:5-37). - If
NODE_ENV === 'travisci'→ forcesqueue.url = 'amqp://localhost:5672'andoptions.rejectUnauthorized = false(:39-42). - Adds
config.get(path, default),config.has(path),config.clone(path, default)dot-path accessors.
bootstrap (bootstrap/process-settings.js, bootstrap/process-listeners.js)
- process-settings (
:1-9): setsprocess.env.TZ = 'Etc/UTC'; ifsettings.allowSelfSignedCertstruthy →NODE_TLS_REJECT_UNAUTHORIZED='0'. - process-listeners (
:1-69): installsuncaughtException,unhandledRejection,exit(logs +logger.shutdown()), and (ifemitExitEvents=false) immediateSIGINT→exit 130 /SIGTERM→exit 143; iftrue(server.js path), delays exit 1000 ms for graceful shutdown. Ifsettings.listenProcessWarning→ logsprocess.on('warning').
7. Configuration
Config is loaded by getconfig, which merges config/default.json with an environment file selected by NODE_ENV/RUNTIME_ENV (e.g. dev.json when RUNTIME_ENV=dev, per supervisor environment=RUNTIME_ENV=dev). Files present: config/default.json, config/dev.json (identical except vhost). No AJV / JSON-schema validation is present in this repo.
Config keys (from config/default.json:1-56, mirrored in README :41-67):
| Key | Default value | Meaning |
|---|---|---|
settings.listenProcessWarning | true | Log Node process.on('warning'). |
settings.allowSelfSignedCerts | (absent → falsy) | If true, disables TLS cert verification globally (process-settings.js:6-8). |
server.queueName | "anti-virus-scanner" | RabbitMQ RPC queue this server listens on. |
tmp.tmpdir | /workspace/antivirus_service/tmp | Dir for scan temp files (passed to tmp-promise). |
queue.url | 3× amqps://avast:avast@nodeN.rabbitmq.local:5671/<vhost> | RabbitMQ cluster URLs (TLS, vhost placeholder). dev.json uses vhost box. |
queue.shuffleUrls | true | Randomize connection order across cluster nodes. |
queue.options.rejectUnauthorized | true | Enforce TLS verification. |
queue.options.ca | ["/workspace/certs/ca_cert.pem"] | CA cert path. |
queue.options.timeout | 10000 | Connect timeout (ms). |
queue.rpcTimeoutMs | 10000 | RPC call timeout. |
queue.rpcQueueMaxSize | 100 | Max queued RPC requests. |
queue.prefetchCount | 1 | One unacked message at a time (serial scanning). |
queue.timeoutMs | 30000 | General queue timeout. |
queue.rpcServer/queueClient/queueServerAssertQueueOptions | { arguments: { x-queue-type: "quorum", delivery-limit: 30 } } | Declares quorum queues with a 30-redelivery limit. |
avast.sockFile | /run/avast/scan.sock | Unix socket of the Avast daemon. |
avast.timeout | 10000 | Avast scan timeout (ms). |
cron.schedule | "0 */3 * * *" | Every 3 hours — VPS update. |
cron.timezone | "Europe/Budapest" | Cron timezone. |
Environment variables (verified usages)
NODE_ENV—config/config.js:39(traviscispecial-case); selects getconfig env file.RUNTIME_ENV— set by supervisor configs (dev/docker); consumed by getconfig to pick the env file.TZ— set toEtc/UTCbyprocess-settings.js:4(overrides any inherited value at startup).NODE_TLS_REJECT_UNAUTHORIZED— set to'0'only ifsettings.allowSelfSignedCerts(process-settings.js:7).SONAR_TOKEN,SONAR_HOST_URL,SONAR_LOGIN,TRAVIS_BRANCH— CI only (.travis.yml:24).
Default config has placeholders, not real endpoints
queue.urlcontains<vhost>and credentialsavast:avast, and CA/cert paths under/workspace/.... These are templates — real deployments override them (per-customer config likely lives in customization branches; see customization-branch-catalog).
8. Tests
There are no automated tests
No
test/directory exists (ls test→ not found). Thetestnpm script is lint-only (package.json:10:npm run lint --silent && echo 'npm test: OK').sonar-project.properties:6even excludestest/**(a dir that doesn’t exist here).eslint-plugin-jestis a devDependency but no Jest config or spec files are present.
CI pipeline — .travis.yml → scripts/travis.sh (:1-27):
yarn install --production=false --force --frozen-lockfile.yarn audit; fail only if severity exit code ≥ 16 (critical) (:14).npm test --silent(= lint).git diff --exit-code(working tree must be clean after install, excluding.yarnrc).- Executable-location guard: fails if any executable file exists outside whitelisted dirs (
^./bin/|^./.git/|^./node_modules/|^./test/scripts/|^./test/customization/travis/|^./customization/bin/,:21). - Separate Travis stage runs SonarCloud scanner via Docker (
.travis.yml:18-24).
CI references customization paths absent from base branch
scripts/travis.sh:21whitelists./customization/bin/and./test/customization/travis/, andbuild.ignore/.eslintignorepatterns hint at atest/tree — none exist ondevel. These are inherited from the shared FaceKom service template and become relevant on customization branches. See customization-architecture.
9. Dependencies on other FaceKom services
Evidenced in code/config:
- RabbitMQ (required) — the service’s entire transport. Connects to an amqps cluster (
config/default.json:11-16) and serves RPC on queueanti-virus-scanner(config/default.json:6,server.js:18). Queues are declared as quorum type withdelivery-limit: 30(config/default.json:29-46). On connection close the server exits (code 2) and supervisord restarts it (server.js:20-23). - Avast daemon (required, local) — not a FaceKom service but an external dependency: communicated with over the Unix socket
/run/avast/scan.sock(config/default.json:49) via@techteamer/avastscan. The daemon itself is started byavast.shunder supervisord. VPS updates shell out to/usr/libexec/avast/vpsupdate(cron/AvastVPSUpdateCronJob.js:53,avast.sh:3). - Callers (clients): Any FaceKom service can depend on this one by importing
AntiVirusScannerRPCClientand callingscanFile(...). The concrete callers are not in this repo (Unverified — see gaps).
No HTTP server, no DB, no Redis
There is no Express/HTTP listener, no database driver, and no Redis client anywhere in the dependency list (
package.json:29-37) or code. Communication is RabbitMQ RPC + a local Unix socket only.
10. Verified gotchas (summary)
Collected pitfalls (all source-verified)
- No Dockerfile on
devel— runtime is supervisord-managed processes; containerization lives elsewhere (§3).- “Tests” = lint only —
npm testruns ESLint; no unit tests exist (§8).- Per-request Avast connection —
AntiVirusScannerRPCServer._callbacknews upAvastServiceevery message;prefetchCount: 1means serial scanning (rpc_server/...:36,config/default.json:27).AvastServicefinally-block bug —tempFile.cleanup()is called even if temp creation threw (tempFileundefined) (avast/AvastService.js:14-23).- Cron lifecycle bugs —
this.nameisundefinedin error logs;stop()/destroy()/reload()callthis.logger.logger.info(double.logger) and would throw (cron/AvastVPSUpdateCronJob.js:27,69,75).- Config placeholders — default
queue.urlhas<vhost>andavast:avastcreds; must be overridden (config/default.json:11-16).TZforced to UTC at startup despite crontimezone: Europe/Budapest— cron honors its owntimezoneoption, process TZ is UTC (process-settings.js:4,cron/...:32).- JSON config syntax errors hard-exit(2) with a custom pretty-printed message (
config/config.js:5-37).README.md:53examplequeue.urlis a singleamqp://localhost:5672string, but the real configs use an array ofamqps://cluster URLs — the README is simplified.
Unverified / gaps
@techteamer/avastscanand@techteamer/mqinternals — not in this repo (nonode_modulescloned). The exact scan-result fields,Avast.scanFile()behavior, RPC framing,getAttachment, andConnectionPool/setupQueueManagerssemantics are defined in those packages and are out of scope (third-party). The result shape in §1 comes fromREADME.mdonly.- Concrete client/caller services — which FaceKom services call
anti-virus-scanneris not evidenced in this repo. - Customization branches — only
develis present locally; CI references (customization/bin/,test/customization/travis/) andbuild.ignoreimply a customization layer not on this branch. See customization-architecture / customization-branch-catalog. yarn.lock— present (84.5 KB) but not read line-by-line; treated as a generated lockfile..editorconfig— present, not transcribed (editor formatting only).- No Git-LFS / model-weight / binary artifacts exist in this repo (confirmed via
git ls-files); nothing to record there.
Sources
Files read in full (entire repo, 29 tracked files):
README.mdpackage.jsonserver.js,cron.js,logger.js,avast.shavast/AvastService.jsrpc_server/AntiVirusScannerRPCServer.jsrpc_client/AntiVirusScannerRPCClient.jscron/AvastVPSUpdateCronJob.jsbootstrap/process-settings.js,bootstrap/process-listeners.jsconfig/config.js,config/default.json,config/dev.jsonbin/credits,bin/.eslintrc.jsonscripts/travis.shsupervisor_dev.conf,supervisor_docker.conf.travis.yml,sonar-project.properties,build.ignore.eslintrc.json,.eslintignore,.gitignoretmp/.gitkeep- (
yarn.lock,.editorconfignoted but not transcribed)
Commands: git -C ... branch -a, git log -1, git ls-files (29 files; no binary/LFS), ls of every top-level dir, ls Dockerfile* docker-compose* (none), ls test (none). Repo: /Users/levander/coding/facekom-v2-clones/antivirus_service @ devel (HEAD 9c6d7d5).