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-5 and package.json:1-3.

  • Repo: /Users/levander/coding/facekom-v2-clones/antivirus_service
  • Base branch: devel (only branch present; git branch -adevel, 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); no node_modules, no Dockerfile, no test/ dir, no Git-LFS / binary blobs (git ls-files contains 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 Map with key file → file Buffer (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):

  1. server.js — the RPC scanner server.
  2. cron.js — periodic Avast VPS (Virus Protection Stream / definitions) updater.

2. Tech stack & runtime

AspectValueSource
LanguageJavaScript (CommonJS, require)all *.js; .eslintrc.json:20 "commonjs": true
RuntimeNode.js >= 16.14.2package.json:13-15 "engines": { "node": ">=16.14.2" }
CI NodeNode 16 on Ubuntu focal.travis.yml:6-7
Package managerYarn (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.0package.json:30
Cron schedulercronosjs ^1.7.1package.json:32
Config loadergetconfig ^4.5.0 (NODE_ENV / RUNTIME_ENV based)package.json:33, config/config.js:6
Logginglog4js ^6.5.2package.json:35, logger.js
Temp filestmp-promise ^3.0.3package.json:36, avast/AvastService.js:3
License reportlicense-checker ^25.0.1package.json:34, bin/credits
LintESLint ^8 + eslint-config-standard ^16 (+ node/import/promise/jest plugins)package.json:22-27
"private": truenot published to npmpackage.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 starting node server.js / node cron.js.


3. Build & run

package.json scripts (verbatim, package.json:8-12)

ScriptCommandWhat it does
linteslint .Lints the whole repo with the root .eslintrc.json (standard + node/recommended).
testnpm 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.
creditsnode bin/credits > CREDITS.htmlGenerates 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):
    1. bootstrap/process-settings() then bootstrap/process-listeners(true) (true = graceful 1 s SIGTERM/SIGINT delay).
    2. Build ConnectionPool from @techteamer/mq with defaultConnectionName: 'default'.
    3. setupQueueManagers(config.get('queue')), get the default connection.
    4. queue.getRPCServer(config.get('server.queueName'), AntiVirusScannerRPCServer) — binds the server class to the queue name anti-virus-scanner.
    5. On RabbitMQ connection 'close' → log + process.exit(2) (relies on supervisord restart).
    6. connectionPool.connect().
  • cron.js (definitions-updater process, cron.js:1-11): bootstraps, then new 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 base devel branch ships no container build. Runtime is expected to be a host/VM (likely customization branches add packaging — see customization-branch-catalog). The supervisor configs reference RUNTIME_ENV=docker and 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):

programcommandnotes
antivirus_servicenode server.jsautostart/autorestart, exitcodes=0,2, stopsignal=TERM, log → /var/log/antivirus_service.log
avast_vps_update_cronnode cron.jssame restart policy, log → /var/log/avast_vps_update_cron.log
avast/workspace/antivirus_service/avast.shruns 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.sock

Start command summary

Production/dev start is not npm start (no such script). It is supervisord launching node server.js, node cron.js, and avast.sh as 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.)

PathWhat it is
server.jsRPC server process entrypoint (queue → scanner).
cron.jsCron process entrypoint (Avast VPS auto-update).
logger.jslog4js singleton; picks channel name from argv[1] (server.jsantivirus-scanner-service, cron.jsavast-vps-update).
avast.shShell launcher for the Avast daemon (vpsupdate then avast -vv).
avast/AvastService.jsWraps @techteamer/avastscan Avast; writes attachment to temp file and scans it.
rpc_server/AntiVirusScannerRPCServer.jsRPC handler: validates message, pulls file attachment, invokes AvastService.
rpc_client/AntiVirusScannerRPCClient.jsClient SDK (scanFile(fileName, id, fileBuffer, timeoutMs)) for other services to call this one.
cron/AvastVPSUpdateCronJob.jscronosjs job; on schedule runs sh /usr/libexec/avast/vpsupdate.
bootstrap/process-settings.jsSets TZ=Etc/UTC; optional self-signed-cert TLS bypass.
bootstrap/process-listeners.jsGlobal uncaughtException/unhandledRejection/exit/signal/warning handlers.
config/config.jsgetconfig wrapper adding .get/.has/.clone; pretty-prints JSON syntax errors; travisci override.
config/default.jsonDefault config (prod-ish: 3-node amqps cluster placeholders, quorum queues).
config/dev.jsonDev config (same as default but vhost box instead of <vhost>).
bin/creditsNode CLI generating CREDITS.html license report.
bin/.eslintrc.jsonESLint override for bin/ (allows console).
scripts/travis.shCI script: install, yarn audit, npm test, git-clean check, executable-location check.
tmp/.gitkeepKeeps the temp dir (scan temp files written here per config).
supervisor_dev.conf / supervisor_docker.confsupervisord process definitions (3 programs each).
.travis.ymlTravis CI pipeline (Node 16 + SonarCloud scan stage).
sonar-project.propertiesSonarCloud project key TechTeamer_antivirus_service, org techteamer.
build.ignoreFiles excluded from production runtime build (dev config, tests, eslint, docs…).
.eslintrc.json / .eslintignore / .editorconfig / .gitignoreLint & editor & VCS config.
yarn.lockYarn lockfile (dependency pin).
README.mdUsage, 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’s init({ start: process.cwd(), production: false }) (promisified).
    • Emits an HTML <table> listing every dependency’s Name / License / repo URL / Publisher to stdout.
    • Invoked by the credits npm script which redirects output to CREDITS.html.
  • bin/.eslintrc.json (bin/.eslintrc.json:1-8): not a script — an ESLint override turning no-console off for bin/ and setting env.node: true (because bin/credits legitimately uses console.log).

bin/ is whitelisted for executables

CI’s scripts/travis.sh:21 lists ^./bin/ in IGNORED_EXECUTABLES, so files under bin/ 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):

  1. Destructures { fileName, id } from the message body.
  2. If either is missing → returns { error: true, message: 'Missing file metadata...' } (does not throw).
  3. request.getAttachment('file'); if absent → returns { error: true, message: 'Did not recieve any attachment...' }.
  4. Reads config.get('avast'), builds new AvastService(avastConfig.sockFile, avastConfig.timeout, this.logger), and returns scanner.scanAttachment(fileName, attachment).

A new AvastService/socket connection is created per request

The server instantiates AvastService inside _callback on every message (:36), so there is no pooled/long-lived Avast socket connection. Combined with prefetchCount: 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 from Avast), and always tempFile.cleanup() in finally. On error logs and throws Error('Error during scanning the following file: ${fileName}.').
  • _createTempFile(buffer, options): tmp.file(options) then fs.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 failed

If _createTempFile throws, tempFile stays undefined and the finally block calls tempFile.cleanup() (:22) → a TypeError would mask the original error. Verified at avast/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(): validates this.schedule via cronosjs.validate; schedules a task with { missingHour: 'insert', skipRepeatedHour: true, timezone }. Tracks status (stopped/scheduled/running).
  • run(): await exec('sh /usr/libexec/avast/vpsupdate') (promisified child_process.exec), logs stdout; logs stderr as error; catches and logs failures.
  • stop()/destroy()/reload() lifecycle helpers.

Two real bugs in the cron job

  1. run()’s catch handler logs this.name (:27) but name is never set on the instance → always undefined.
  2. stop() and destroy() call this.logger.logger.info(...) (:69, :75) — double .logger. Since logger is the log4js logger (no .logger property), calling stop()/destroy()/reload() would throw TypeError. 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.jsantivirus-scanner-service; cron.jsavast-vps-update; else unknown. Exposes logger.shutdown = () => log4js.shutdown().

config/config.js (:1-91)

Wraps getconfig. Notable behaviors:

  • Catches getconfig SyntaxError, parses the bad file path + offset from the message, prints the offending ~40-char JSON slice to console.error, and process.exit(2) (:5-37).
  • If NODE_ENV === 'travisci' → forces queue.url = 'amqp://localhost:5672' and options.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): sets process.env.TZ = 'Etc/UTC'; if settings.allowSelfSignedCerts truthy → NODE_TLS_REJECT_UNAUTHORIZED='0'.
  • process-listeners (:1-69): installs uncaughtException, unhandledRejection, exit (logs + logger.shutdown()), and (if emitExitEvents=false) immediate SIGINT→exit 130 / SIGTERM→exit 143; if true (server.js path), delays exit 1000 ms for graceful shutdown. If settings.listenProcessWarning → logs process.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):

KeyDefault valueMeaning
settings.listenProcessWarningtrueLog 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/tmpDir for scan temp files (passed to tmp-promise).
queue.urlamqps://avast:avast@nodeN.rabbitmq.local:5671/<vhost>RabbitMQ cluster URLs (TLS, vhost placeholder). dev.json uses vhost box.
queue.shuffleUrlstrueRandomize connection order across cluster nodes.
queue.options.rejectUnauthorizedtrueEnforce TLS verification.
queue.options.ca["/workspace/certs/ca_cert.pem"]CA cert path.
queue.options.timeout10000Connect timeout (ms).
queue.rpcTimeoutMs10000RPC call timeout.
queue.rpcQueueMaxSize100Max queued RPC requests.
queue.prefetchCount1One unacked message at a time (serial scanning).
queue.timeoutMs30000General 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.sockUnix socket of the Avast daemon.
avast.timeout10000Avast scan timeout (ms).
cron.schedule"0 */3 * * *"Every 3 hours — VPS update.
cron.timezone"Europe/Budapest"Cron timezone.

Environment variables (verified usages)

  • NODE_ENVconfig/config.js:39 (travisci special-case); selects getconfig env file.
  • RUNTIME_ENV — set by supervisor configs (dev/docker); consumed by getconfig to pick the env file.
  • TZset to Etc/UTC by process-settings.js:4 (overrides any inherited value at startup).
  • NODE_TLS_REJECT_UNAUTHORIZED — set to '0' only if settings.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.url contains <vhost> and credentials avast: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). The test npm script is lint-only (package.json:10: npm run lint --silent && echo 'npm test: OK'). sonar-project.properties:6 even excludes test/** (a dir that doesn’t exist here). eslint-plugin-jest is a devDependency but no Jest config or spec files are present.

CI pipeline.travis.ymlscripts/travis.sh (:1-27):

  1. yarn install --production=false --force --frozen-lockfile.
  2. yarn audit; fail only if severity exit code ≥ 16 (critical) (:14).
  3. npm test --silent (= lint).
  4. git diff --exit-code (working tree must be clean after install, excluding .yarnrc).
  5. Executable-location guard: fails if any executable file exists outside whitelisted dirs (^./bin/|^./.git/|^./node_modules/|^./test/scripts/|^./test/customization/travis/|^./customization/bin/, :21).
  6. Separate Travis stage runs SonarCloud scanner via Docker (.travis.yml:18-24).

CI references customization paths absent from base branch

scripts/travis.sh:21 whitelists ./customization/bin/ and ./test/customization/travis/, and build.ignore/.eslintignore patterns hint at a test/ tree — none exist on devel. 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 queue anti-virus-scanner (config/default.json:6, server.js:18). Queues are declared as quorum type with delivery-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 by avast.sh under 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 AntiVirusScannerRPCClient and calling scanFile(...). 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 onlynpm test runs ESLint; no unit tests exist (§8).
  • Per-request Avast connectionAntiVirusScannerRPCServer._callback news up AvastService every message; prefetchCount: 1 means serial scanning (rpc_server/...:36, config/default.json:27).
  • AvastService finally-block bugtempFile.cleanup() is called even if temp creation threw (tempFile undefined) (avast/AvastService.js:14-23).
  • Cron lifecycle bugsthis.name is undefined in error logs; stop()/destroy()/reload() call this.logger.logger.info (double .logger) and would throw (cron/AvastVPSUpdateCronJob.js:27,69,75).
  • Config placeholders — default queue.url has <vhost> and avast:avast creds; must be overridden (config/default.json:11-16).
  • TZ forced to UTC at startup despite cron timezone: Europe/Budapest — cron honors its own timezone option, 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:53 example queue.url is a single amqp://localhost:5672 string, but the real configs use an array of amqps:// cluster URLs — the README is simplified.

Unverified / gaps

  • @techteamer/avastscan and @techteamer/mq internals — not in this repo (no node_modules cloned). The exact scan-result fields, Avast.scanFile() behavior, RPC framing, getAttachment, and ConnectionPool/setupQueueManagers semantics are defined in those packages and are out of scope (third-party). The result shape in §1 comes from README.md only.
  • Concrete client/caller services — which FaceKom services call anti-virus-scanner is not evidenced in this repo.
  • Customization branches — only devel is present locally; CI references (customization/bin/, test/customization/travis/) and build.ignore imply 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.md
  • package.json
  • server.js, cron.js, logger.js, avast.sh
  • avast/AvastService.js
  • rpc_server/AntiVirusScannerRPCServer.js
  • rpc_client/AntiVirusScannerRPCClient.js
  • cron/AvastVPSUpdateCronJob.js
  • bootstrap/process-settings.js, bootstrap/process-listeners.js
  • config/config.js, config/default.json, config/dev.json
  • bin/credits, bin/.eslintrc.json
  • scripts/travis.sh
  • supervisor_dev.conf, supervisor_docker.conf
  • .travis.yml, sonar-project.properties, build.ignore
  • .eslintrc.json, .eslintignore, .gitignore
  • tmp/.gitkeep
  • (yarn.lock, .editorconfig noted 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).