├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── codeql-analysis.yml │ └── test.yml ├── .gitignore ├── .markdownlint.json ├── .prettierignore ├── .prettierrc ├── API.md ├── HISTORY.md ├── LICENSE ├── Makefile ├── README.md ├── examples ├── basic_async_await.js ├── passthrough.js ├── pipe2s3.js ├── stream.js └── version.js ├── index.js ├── jsdoc.json ├── lib ├── NodeClamError.js ├── NodeClamTransform.js └── getFiles.js ├── package-lock.json ├── package.json └── tests ├── bad_files_list.txt ├── clamd.conf ├── eicargen.js ├── good_files_list.txt ├── good_scan_dir ├── empty_file.txt ├── good_file_1.txt └── good_file_2.txt ├── index.js ├── stunnel.conf └── test_config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "extends": [ 9 | "airbnb-base", 10 | "plugin:prettier/recommended", 11 | "plugin:chai-friendly/recommended", 12 | "plugin:jsdoc/recommended" 13 | ], 14 | "plugins": ["prettier", "chai-friendly", "jsdoc"], 15 | "globals": { 16 | "Atomics": "readonly", 17 | "SharedArrayBuffer": "readonly" 18 | }, 19 | "parserOptions": { 20 | "ecmaVersion": 2022, 21 | "sourceType": "module" 22 | }, 23 | "rules": { 24 | "no-console": "off", 25 | "no-param-reassign": "off", 26 | "prettier/prettier": "error", 27 | "class-methods-use-this": "off", 28 | "require-jsdoc": "error", 29 | "valid-jsdoc": "off", 30 | "global-require": "warn", 31 | "lines-between-class-members": "off", 32 | "no-underscore-dangle": "off", 33 | "no-promise-executor-return": "off", 34 | "jsdoc/check-alignment": 1, // Recommended 35 | "jsdoc/check-indentation": 1, 36 | "jsdoc/check-param-names": 1, // Recommended 37 | "jsdoc/check-syntax": 1, 38 | "jsdoc/check-tag-names": [ 39 | "warn", 40 | { 41 | "definedTags": [ 42 | "typicalname", 43 | "route", 44 | "authentication", 45 | "bodyparam", 46 | "routeparam", 47 | "queryparam" 48 | ] 49 | } 50 | ], 51 | "jsdoc/check-types": 1, // Recommended 52 | "jsdoc/implements-on-classes": 1, // Recommended 53 | "jsdoc/match-description": 1, 54 | "jsdoc/tag-lines": [ 55 | "error", 56 | "never", 57 | { 58 | "startLines": 1 59 | } 60 | ], 61 | "jsdoc/require-description": 1, 62 | "jsdoc/require-hyphen-before-param-description": 1, 63 | "jsdoc/require-jsdoc": 1, // Recommended 64 | "jsdoc/require-param": 1, // Recommended 65 | "jsdoc/require-param-description": 1, // Recommended 66 | "jsdoc/require-param-name": 1, // Recommended 67 | "jsdoc/require-param-type": 1, // Recommended 68 | "jsdoc/require-returns": 1, // Recommended 69 | "jsdoc/require-returns-check": 1, // Recommended 70 | "jsdoc/require-returns-description": 1, // Recommended 71 | "jsdoc/require-returns-type": 1, // Recommended 72 | "jsdoc/valid-types": 1, // Recommended 73 | "jsdoc/no-defaults": 0, // Recommended 74 | "jsdoc/check-access": 1, // Recommended 75 | "jsdoc/check-property-names": 1, // Recommended 76 | "jsdoc/check-values": 1, // Recommended 77 | "jsdoc/empty-tags": 1, // Recommended 78 | "jsdoc/multiline-blocks": 1, // Recommended 79 | "jsdoc/no-multi-asterisks": 1, // Recommended 80 | "jsdoc/require-property": 1, // Recommended 81 | "jsdoc/require-property-description": 1, // Recommended 82 | "jsdoc/require-property-name": 1, // Recommended 83 | "jsdoc/require-property-type": 1, // Recommended 84 | "jsdoc/require-yields": 1, // Recommended 85 | "jsdoc/require-yields-check": 1 // Recommended 86 | } 87 | } -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '34 6 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/build.yml 2 | name: Clamscan Test Suite 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | test: 10 | runs-on: ubuntu-22.04 11 | strategy: 12 | matrix: 13 | node-version: [16,18,20,21,22] 14 | steps: 15 | - name: Update Apt 16 | run: sudo apt-get update 17 | - name: Install ClamAV 18 | run: sudo apt-get install clamav clamav-daemon 19 | - name: Restart Freshclam 20 | run: sudo systemctl restart clamav-freshclam 21 | - name: Wait for freshclam to be up to date 22 | run: | 23 | until sudo grep "$(date | cut -c -10)" /var/log/clamav/freshclam.log | grep -Eq 'Clamd was NOT notified|Clamd successfully notified about the update.'; do sleep 1; done; 24 | sudo tail /var/log/clamav/freshclam.log 25 | - name: Remove Syslog from ClamD Config & Restard ClamD 26 | run: | 27 | sudo systemctl stop clamav-daemon; 28 | sudo sed -i /syslog/d /lib/systemd/system/clamav-daemon.service; 29 | sudo systemctl daemon-reload; 30 | cat /lib/systemd/system/clamav-daemon.service; 31 | sudo systemctl start clamav-daemon; 32 | - name: Install OpenSSL 33 | run: sudo apt-get install openssl 34 | - name: Generate Key Pair for TLS 35 | run: openssl req -new -sha256 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=localhost" -addext "subjectAltName = DNS:localhost,IP:127.0.0.1,IP:::1" -newkey ed25519 -keyout key.pem -nodes -x509 -days 365 -out cert.pem 36 | - name: Install stunnel 37 | run: sudo apt-get install stunnel4 38 | - name: Install / Trust certificate 39 | run: | 40 | sudo cp cert.pem /usr/local/share/ca-certificates/snakeoil.crt 41 | sudo update-ca-certificates 42 | sudo cp cert.pem /etc/stunnel/cert.pem 43 | sudo cp key.pem /etc/stunnel/key.pem 44 | - name: Checkout repository 45 | uses: actions/checkout@v3 46 | - name: Set stunnel config 47 | run: | 48 | sudo cp tests/stunnel.conf /etc/stunnel/ 49 | sudo sed -i "s/\/var\/run\/clamd.scan\/clamd.sock/$(sudo cat /etc/clamav/clamd.conf |grep "LocalSocket "|cut -d " " -f 2 | sed 's/\//\\\//g')/" /etc/stunnel/stunnel.conf 50 | - name: Restart stunnel 51 | run: | 52 | sudo systemctl restart stunnel4; 53 | sudo ss -tlnp; 54 | - name: Open ~ for all users to read 55 | run: chmod 755 ~ 56 | - name: Use Node.js ${{ matrix.node-version }} 57 | uses: actions/setup-node@v3 58 | with: 59 | node-version: ${{ matrix.node-version }} 60 | - name: Install dependencies 61 | run: npm ci 62 | - name: Wait for ClamD Socket 63 | run: | 64 | sudo systemctl status clamav-daemon 65 | until [ -S $(cat /etc/clamav/clamd.conf |grep "LocalSocket "|cut -d ' ' -f 2) ]; do sleep 1; done 66 | - name: Run tests 67 | run: npm test 68 | env: 69 | NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/snakeoil.crt 70 | - name: debug? 71 | if: ${{ failure() }} 72 | run: | 73 | sudo journalctl -e -u stunnel4; 74 | sudo journalctl -e -u clamav-daemon; 75 | echo 'PING' | openssl s_client --connect localhost:3311 -ign_eof; 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | node_modules 14 | 15 | npm-debug.log 16 | tests/clamscan-log 17 | tests/infected 18 | tests/bad_scan_dir 19 | tests/mixed_scan_dir 20 | tests/good_files_list_tmp.txt 21 | tests/output 22 | 23 | # Mac/OSX 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "no-inline-html": { 4 | "allowed_elements": [ 5 | "a" 6 | ] 7 | }, 8 | "MD025": false, 9 | "MD024": false 10 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "trailingComma": "es5", 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## Classes 2 | 3 |
4 |
NodeClam
5 |

NodeClam class definition.

6 |
7 |
NodeClamError
8 |

Clamscan-specific extension of the Javascript Error object

9 |

NOTE: If string is passed to first param, it will be msg and data will be {}

10 |
11 |
NodeClamTransform
12 |

A NodeClam - specific Transform extension that coddles 13 | chunks into the correct format for a ClamAV socket.

14 |
15 |
16 | 17 | ## Members 18 | 19 |
20 |
pingPromise.<object>
21 |

Quick check to see if the remote/local socket is working. Callback/Resolve 22 | response is an instance to a ClamAV socket client.

23 |
24 |
25 | 26 | ## Functions 27 | 28 |
29 |
getFiles(dir, [recursive])Array
30 |

Gets a listing of all files (no directories) within a given path. 31 | By default, it will retrieve files recursively.

32 |
33 |
34 | 35 | 36 | 37 | ## NodeClam 38 | NodeClam class definition. 39 | 40 | **Kind**: global class 41 | **Access**: public 42 | 43 | * [NodeClam](#NodeClam) 44 | * [new NodeClam()](#new_NodeClam_new) 45 | * [.init([options], [cb])](#NodeClam+init) ⇒ Promise.<object> 46 | * [.reset([options], [cb])](#NodeClam+reset) ⇒ Promise.<object> 47 | * [.getVersion([cb])](#NodeClam+getVersion) ⇒ Promise.<string> 48 | * [.isInfected(file, [cb])](#NodeClam+isInfected) ⇒ Promise.<object> 49 | * [.passthrough()](#NodeClam+passthrough) ⇒ Transform 50 | * [.scanFile(file, [cb])](#NodeClam+scanFile) ⇒ Promise.<object> 51 | * [.scanFiles(files, [endCb], [fileCb])](#NodeClam+scanFiles) ⇒ Promise.<object> 52 | * [.scanDir(path, [endCb], [fileCb])](#NodeClam+scanDir) ⇒ Promise.<object> 53 | * [.scanStream(stream, [cb])](#NodeClam+scanStream) ⇒ Promise.<object> 54 | 55 | 56 | 57 | ### new NodeClam() 58 | This sets up all the defaults of the instance but does not 59 | necessarily return an initialized instance. Use `.init` for that. 60 | 61 | 62 | 63 | ### nodeClam.init([options], [cb]) ⇒ Promise.<object> 64 | Initialization method. 65 | 66 | **Kind**: instance method of [NodeClam](#NodeClam) 67 | **Returns**: Promise.<object> - An initated instance of NodeClam 68 | **Access**: public 69 | 70 | | Param | Type | Default | Description | 71 | | --- | --- | --- | --- | 72 | | [options] | object | | User options for the Clamscan module | 73 | | [options.removeInfected] | boolean | false | If true, removes infected files when found | 74 | | [options.quarantineInfected] | boolean \| string | false | If not false, should be a string to a path to quarantine infected files | 75 | | [options.scanLog] | string | null | Path to a writeable log file to write scan results into | 76 | | [options.debugMode] | boolean | false | If true, *a lot* of info will be spewed to the logs | 77 | | [options.fileList] | string | null | Path to file containing list of files to scan (for `scanFiles` method) | 78 | | [options.scanRecursively] | boolean | true | If true, deep scan folders recursively (for `scanDir` method) | 79 | | [options.clamscan] | object | | Options specific to the clamscan binary | 80 | | [options.clamscan.path] | string | "'/usr/bin/clamscan'" | Path to clamscan binary on your server | 81 | | [options.clamscan.db] | string | null | Path to a custom virus definition database | 82 | | [options.clamscan.scanArchives] | boolean | true | If true, scan archives (ex. zip, rar, tar, dmg, iso, etc...) | 83 | | [options.clamscan.active] | boolean | true | If true, this module will consider using the clamscan binary | 84 | | [options.clamdscan] | object | | Options specific to the clamdscan binary | 85 | | [options.clamdscan.socket] | string | false | Path to socket file for connecting via TCP | 86 | | [options.clamdscan.host] | string | false | IP of host to connec to TCP interface | 87 | | [options.clamdscan.port] | string | false | Port of host to use when connecting via TCP interface | 88 | | [options.clamdscan.timeout] | number | 60000 | Timeout for scanning files | 89 | | [options.clamdscan.localFallback] | boolean | false | If false, do not fallback to a local binary-method of scanning | 90 | | [options.clamdscan.path] | string | "'/usr/bin/clamdscan'" | Path to the `clamdscan` binary on your server | 91 | | [options.clamdscan.configFile] | string | null | Specify config file if it's in an usual place | 92 | | [options.clamdscan.multiscan] | boolean | true | If true, scan using all available cores | 93 | | [options.clamdscan.reloadDb] | boolean | false | If true, will re-load the DB on ever call (slow) | 94 | | [options.clamdscan.active] | boolean | true | If true, this module will consider using the `clamdscan` binary | 95 | | [options.clamdscan.bypassTest] | boolean | false | If true, check to see if socket is avaliable | 96 | | [options.clamdscan.tls] | boolean | false | If true, connect to a TLS-Termination proxy in front of ClamAV | 97 | | [options.preference] | object | 'clamdscan' | If preferred binary is found and active, it will be used by default | 98 | | [cb] | function | | Callback method. Prototype: `(err, )` | 99 | 100 | **Example** 101 | ```js 102 | const NodeClam = require('clamscan'); 103 | const ClamScan = new NodeClam().init({ 104 | removeInfected: false, 105 | quarantineInfected: false, 106 | scanLog: null, 107 | debugMode: false, 108 | fileList: null, 109 | scanRecursively: true, 110 | clamscan: { 111 | path: '/usr/bin/clamscan', 112 | db: null, 113 | scanArchives: true, 114 | active: true 115 | }, 116 | clamdscan: { 117 | socket: false, 118 | host: false, 119 | port: false, 120 | timeout: 60000, 121 | localFallback: false, 122 | path: '/usr/bin/clamdscan', 123 | configFile: null, 124 | multiscan: true, 125 | reloadDb: false, 126 | active: true, 127 | bypassTest: false, 128 | }, 129 | preference: 'clamdscan' 130 | }); 131 | ``` 132 | 133 | 134 | ### nodeClam.reset([options], [cb]) ⇒ Promise.<object> 135 | Allows one to create a new instances of clamscan with new options. 136 | 137 | **Kind**: instance method of [NodeClam](#NodeClam) 138 | **Returns**: Promise.<object> - A reset instance of NodeClam 139 | **Access**: public 140 | 141 | | Param | Type | Default | Description | 142 | | --- | --- | --- | --- | 143 | | [options] | object | {} | Same options as the `init` method | 144 | | [cb] | function | | What to do after reset (repsponds with reset instance of NodeClam) | 145 | 146 | 147 | 148 | ### nodeClam.getVersion([cb]) ⇒ Promise.<string> 149 | Establish the clamav version of a local or remote clamav daemon. 150 | 151 | **Kind**: instance method of [NodeClam](#NodeClam) 152 | **Returns**: Promise.<string> - - The version of ClamAV that is being interfaced with 153 | **Access**: public 154 | 155 | | Param | Type | Description | 156 | | --- | --- | --- | 157 | | [cb] | function | What to do when version is established | 158 | 159 | **Example** 160 | ```js 161 | // Callback example 162 | clamscan.getVersion((err, version) => { 163 | if (err) return console.error(err); 164 | console.log(`ClamAV Version: ${version}`); 165 | }); 166 | 167 | // Promise example 168 | const clamscan = new NodeClam().init(); 169 | const version = await clamscan.getVersion(); 170 | console.log(`ClamAV Version: ${version}`); 171 | ``` 172 | 173 | 174 | ### nodeClam.isInfected(file, [cb]) ⇒ Promise.<object> 175 | This method allows you to scan a single file. It supports a callback and Promise API. 176 | If no callback is supplied, a Promise will be returned. This method will likely 177 | be the most common use-case for this module. 178 | 179 | **Kind**: instance method of [NodeClam](#NodeClam) 180 | **Returns**: Promise.<object> - Object like: `{ file: String, isInfected: Boolean, viruses: Array }` 181 | **Access**: public 182 | 183 | | Param | Type | Default | Description | 184 | | --- | --- | --- | --- | 185 | | file | string | | Path to the file to check | 186 | | [cb] | function | | What to do after the scan | 187 | 188 | **Example** 189 | ```js 190 | // Callback Example 191 | clamscan.isInfected('/a/picture/for_example.jpg', (err, file, isInfected, viruses) => { 192 | if (err) return console.error(err); 193 | 194 | if (isInfected) { 195 | console.log(`${file} is infected with ${viruses.join(', ')}.`); 196 | } 197 | }); 198 | 199 | // Promise Example 200 | clamscan.isInfected('/a/picture/for_example.jpg').then(result => { 201 | const {file, isInfected, viruses} = result; 202 | if (isInfected) console.log(`${file} is infected with ${viruses.join(', ')}.`); 203 | }).then(err => { 204 | console.error(err); 205 | }); 206 | 207 | // Async/Await Example 208 | const {file, isInfected, viruses} = await clamscan.isInfected('/a/picture/for_example.jpg'); 209 | ``` 210 | 211 | 212 | ### nodeClam.passthrough() ⇒ Transform 213 | Returns a PassthroughStream object which allows you to 214 | pipe a ReadbleStream through it and on to another output. In the case of this 215 | implementation, it's actually forking the data to also 216 | go to ClamAV via TCP or Domain Sockets. Each data chunk is only passed on to 217 | the output if that chunk was successfully sent to and received by ClamAV. 218 | The PassthroughStream object returned from this method has a special event 219 | that is emitted when ClamAV finishes scanning the streamed data (`scan-complete`) 220 | so that you can decide if there's anything you need to do with the final output 221 | destination (ex. delete a file or S3 object). 222 | 223 | **Kind**: instance method of [NodeClam](#NodeClam) 224 | **Returns**: Transform - A Transform stream for piping a Readable stream into 225 | **Access**: public 226 | **Example** 227 | ```js 228 | const NodeClam = require('clamscan'); 229 | 230 | // You'll need to specify your socket or TCP connection info 231 | const clamscan = new NodeClam().init({ 232 | clamdscan: { 233 | socket: '/var/run/clamd.scan/clamd.sock', 234 | host: '127.0.0.1', 235 | port: 3310, 236 | } 237 | }); 238 | 239 | // For example's sake, we're using the Axios module 240 | const axios = require('axios'); 241 | 242 | // Get a readable stream for a URL request 243 | const input = axios.get(someUrl); 244 | 245 | // Create a writable stream to a local file 246 | const output = fs.createWriteStream(someLocalFile); 247 | 248 | // Get instance of this module's PassthroughStream object 249 | const av = clamscan.passthrough(); 250 | 251 | // Send output of Axios stream to ClamAV. 252 | // Send output of Axios to `someLocalFile` if ClamAV receives data successfully 253 | input.pipe(av).pipe(output); 254 | 255 | // What happens when scan is completed 256 | av.on('scan-complete', result => { 257 | const {isInfected, viruses} = result; 258 | // Do stuff if you want 259 | }); 260 | 261 | // What happens when data has been fully written to `output` 262 | output.on('finish', () => { 263 | // Do stuff if you want 264 | }); 265 | 266 | // NOTE: no errors (or other events) are being handled in this example but standard errors will be emitted according to NodeJS's Stream specifications 267 | ``` 268 | 269 | 270 | ### nodeClam.scanFile(file, [cb]) ⇒ Promise.<object> 271 | Just an alias to `isInfected`. See docs for that for usage examples. 272 | 273 | **Kind**: instance method of [NodeClam](#NodeClam) 274 | **Returns**: Promise.<object> - Object like: `{ file: String, isInfected: Boolean, viruses: Array }` 275 | **Access**: public 276 | 277 | | Param | Type | Description | 278 | | --- | --- | --- | 279 | | file | string | Path to the file to check | 280 | | [cb] | function | What to do after the scan | 281 | 282 | 283 | 284 | ### nodeClam.scanFiles(files, [endCb], [fileCb]) ⇒ Promise.<object> 285 | Scans an array of files or paths. You must provide the full paths of the 286 | files and/or paths. Also enables the ability to scan a file list. 287 | 288 | This is essentially a wrapper for isInfected that simplifies the process 289 | of scanning many files or directories. 290 | 291 | **NOTE:** The only way to get per-file notifications is through the callback API. 292 | 293 | **Kind**: instance method of [NodeClam](#NodeClam) 294 | **Returns**: Promise.<object> - Object like: `{ goodFiles: Array, badFiles: Array, errors: Object, viruses: Array }` 295 | **Access**: public 296 | 297 | | Param | Type | Default | Description | 298 | | --- | --- | --- | --- | 299 | | files | Array | | A list of files or paths (full paths) to be scanned | 300 | | [endCb] | function | | What to do after the scan completes | 301 | | [fileCb] | function | | What to do after each file has been scanned | 302 | 303 | **Example** 304 | ```js 305 | // Callback Example 306 | const scanStatus = { 307 | good: 0, 308 | bad: 0 309 | }; 310 | const files = [ 311 | '/path/to/file/1.jpg', 312 | '/path/to/file/2.mov', 313 | '/path/to/file/3.rb' 314 | ]; 315 | clamscan.scanFiles(files, (err, goodFiles, badFiles, viruses) => { 316 | if (err) return console.error(err); 317 | if (badFiles.length > 0) { 318 | console.log({ 319 | msg: `${goodFiles.length} files were OK. ${badFiles.length} were infected!`, 320 | badFiles, 321 | goodFiles, 322 | viruses, 323 | }); 324 | } else { 325 | res.send({msg: "Everything looks good! No problems here!."}); 326 | } 327 | }, (err, file, isInfected, viruses) => { 328 | ;(isInfected ? scanStatus.bad++ : scanStatus.good++); 329 | console.log(`${file} is ${(isInfected ? `infected with ${viruses}` : 'ok')}.`); 330 | console.log('Scan Status: ', `${(scanStatus.bad + scanStatus.good)}/${files.length}`); 331 | }); 332 | 333 | // Async/Await method 334 | const {goodFiles, badFiles, errors, viruses} = await clamscan.scanFiles(files); 335 | ``` 336 | 337 | 338 | ### nodeClam.scanDir(path, [endCb], [fileCb]) ⇒ Promise.<object> 339 | Scans an entire directory. Provides 3 params to end callback: Error, path 340 | scanned, and whether its infected or not. To scan multiple directories, pass 341 | them as an array to the `scanFiles` method. 342 | 343 | This obeys your recursive option even for `clamdscan` which does not have a native 344 | way to turn this feature off. If you have multiple paths, send them in an array 345 | to `scanFiles`. 346 | 347 | NOTE: While possible, it is NOT advisable to use the `fileCb` parameter when 348 | using the `clamscan` binary. Doing so with `clamdscan` is okay, however. This 349 | method also allows for non-recursive scanning with the clamdscan binary. 350 | 351 | **Kind**: instance method of [NodeClam](#NodeClam) 352 | **Returns**: Promise.<object> - Object like: `{ path: String, isInfected: Boolean, goodFiles: Array, badFiles: Array, viruses: Array }` 353 | **Access**: public 354 | 355 | | Param | Type | Default | Description | 356 | | --- | --- | --- | --- | 357 | | path | string | | The directory to scan files of | 358 | | [endCb] | function | | What to do when all files have been scanned | 359 | | [fileCb] | function | | What to do after each file has been scanned | 360 | 361 | **Example** 362 | ```js 363 | // Callback Method 364 | clamscan.scanDir('/some/path/to/scan', (err, goodFiles, badFiles, viruses, numGoodFiles) { 365 | if (err) return console.error(err); 366 | 367 | if (badFiles.length > 0) { 368 | console.log(`${path} was infected. The offending files (${badFiles.map(v => `${v.file} (${v.virus})`).join (', ')}) have been quarantined.`); 369 | console.log(`Viruses Found: ${viruses.join(', ')}`); 370 | } else { 371 | console.log('Everything looks good! No problems here!.'); 372 | } 373 | }); 374 | 375 | // Async/Await Method 376 | const {path, isInfected, goodFiles, badFiles, viruses} = await clamscan.scanDir('/some/path/to/scan'); 377 | ``` 378 | 379 | 380 | ### nodeClam.scanStream(stream, [cb]) ⇒ Promise.<object> 381 | Allows you to scan a binary stream. 382 | 383 | **NOTE:** This method will only work if you've configured the module to allow the 384 | use of a TCP or UNIX Domain socket. In other words, this will not work if you only 385 | have access to a local ClamAV binary. 386 | 387 | **Kind**: instance method of [NodeClam](#NodeClam) 388 | **Returns**: Promise.<object> - Object like: `{ file: String, isInfected: Boolean, viruses: Array } ` 389 | **Access**: public 390 | 391 | | Param | Type | Description | 392 | | --- | --- | --- | 393 | | stream | Readable | A readable stream to scan | 394 | | [cb] | function | What to do when the socket response with results | 395 | 396 | **Example** 397 | ```js 398 | const NodeClam = require('clamscan'); 399 | 400 | // You'll need to specify your socket or TCP connection info 401 | const clamscan = new NodeClam().init({ 402 | clamdscan: { 403 | socket: '/var/run/clamd.scan/clamd.sock', 404 | host: '127.0.0.1', 405 | port: 3310, 406 | } 407 | }); 408 | const Readable = require('stream').Readable; 409 | const rs = Readable(); 410 | 411 | rs.push('foooooo'); 412 | rs.push('barrrrr'); 413 | rs.push(null); 414 | 415 | // Callback Example 416 | clamscan.scanStream(stream, (err, { isInfected, viruses }) => { 417 | if (err) return console.error(err); 418 | if (isInfected) return console.log('Stream is infected! Booo!', viruses); 419 | console.log('Stream is not infected! Yay!'); 420 | }); 421 | 422 | // Async/Await Example 423 | const { isInfected, viruses } = await clamscan.scanStream(stream); 424 | ``` 425 | 426 | 427 | ## NodeClamError 428 | Clamscan-specific extension of the Javascript Error object 429 | 430 | **NOTE**: If string is passed to first param, it will be `msg` and data will be `{}` 431 | 432 | **Kind**: global class 433 | 434 | 435 | ### new NodeClamError(data, ...params) 436 | Creates a new instance of a NodeClamError. 437 | 438 | 439 | | Param | Type | Description | 440 | | --- | --- | --- | 441 | | data | object | Additional data we might want to have access to on error | 442 | | ...params | any | The usual params you'd pass to create an Error object | 443 | 444 | 445 | 446 | ## NodeClamTransform 447 | A NodeClam - specific Transform extension that coddles 448 | chunks into the correct format for a ClamAV socket. 449 | 450 | **Kind**: global class 451 | 452 | * [NodeClamTransform](#NodeClamTransform) 453 | * [new NodeClamTransform(options, debugMode)](#new_NodeClamTransform_new) 454 | * [._transform(chunk, encoding, cb)](#NodeClamTransform+_transform) 455 | * [._flush(cb)](#NodeClamTransform+_flush) 456 | 457 | 458 | 459 | ### new NodeClamTransform(options, debugMode) 460 | Creates a new instance of NodeClamTransorm. 461 | 462 | 463 | | Param | Type | Default | Description | 464 | | --- | --- | --- | --- | 465 | | options | object | | Optional overrides to defaults (same as Node.js Transform) | 466 | | debugMode | boolean | false | If true, do special debug logging | 467 | 468 | 469 | 470 | ### nodeClamTransform.\_transform(chunk, encoding, cb) 471 | Actually does the transorming of the data for ClamAV. 472 | 473 | **Kind**: instance method of [NodeClamTransform](#NodeClamTransform) 474 | 475 | | Param | Type | Description | 476 | | --- | --- | --- | 477 | | chunk | Buffer | The piece of data to push onto the stream | 478 | | encoding | string | The encoding of the chunk | 479 | | cb | function | What to do when done pushing chunk | 480 | 481 | 482 | 483 | ### nodeClamTransform.\_flush(cb) 484 | This will flush out the stream when all data has been received. 485 | 486 | **Kind**: instance method of [NodeClamTransform](#NodeClamTransform) 487 | 488 | | Param | Type | Description | 489 | | --- | --- | --- | 490 | | cb | function | What to do when done | 491 | 492 | 493 | 494 | ## ping ⇒ Promise.<object> 495 | Quick check to see if the remote/local socket is working. Callback/Resolve 496 | response is an instance to a ClamAV socket client. 497 | 498 | **Kind**: global variable 499 | **Returns**: Promise.<object> - A copy of the Socket/TCP client 500 | **Access**: public 501 | 502 | | Param | Type | Description | 503 | | --- | --- | --- | 504 | | [cb] | function | What to do after the ping | 505 | 506 | 507 | 508 | ## getFiles(dir, [recursive]) ⇒ Array 509 | Gets a listing of all files (no directories) within a given path. 510 | By default, it will retrieve files recursively. 511 | 512 | **Kind**: global function 513 | **Returns**: Array - - List of all requested path files 514 | 515 | | Param | Type | Default | Description | 516 | | --- | --- | --- | --- | 517 | | dir | string | | The directory to get all files of | 518 | | [recursive] | boolean | true | If true (default), get all files recursively; False: only get files directly in path | 519 | 520 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | This file is a manually maintained list of changes for each release. Feel free to add your changes here when sending pull requests. Also send corrections if you spot any mistakes. 4 | 5 | ## 0.2.1 6 | 7 | - ClamAV returns an exit code 1 when it detects a virus but `exec` was interpreting that response as an error. Checking the response with type-sensitive equivalence resolves this bug. 8 | 9 | ## 0.2.2 10 | 11 | - Fixed documentation 12 | 13 | ## 0.4.0 (2014-11-19) 14 | 15 | - Corrected the installation instructions for `clamav`. Thank you @jshamley! 16 | - Fixed major bug preventing the `scan_dir` method from working properly 17 | - Corrected documentation describing how to instantiate this module. 18 | 19 | ## 0.5.0 (2014-12-19) 20 | 21 | - Deprecated the `quarantine_path` option. Please only use `quarantine_infected` for now on. 22 | - Updated documentation to reflect above change. 23 | 24 | ## 0.6.0 (2015-01-02) 25 | 26 | **NOTE:** There are some breaking changes on this release. Since this is still a pre-version 1 release, I decided to only do a minor bump to 0.4.0 27 | 28 | - The ability to run "forked" instances of `clamscan` has been removed because of irregularities with different systems--namely if you had `max_forks` set to 3, it would sometimes only scan the first or last file in the group... not good. 29 | - Added the ability to use `clamdscan`. This ultimately negates the downside of removing the forking capability mentioned in item one. This is a really big improvement (many orders of magnitude) if your system has access to the `clamdscan` daemon. 30 | - Added a `file_list` option allowing one to specify a text file that lists (one per line) paths to files to be scanned. This is great if you need to scan hundreds or thousands of random files. 31 | - `clam_path` option has been moved to `clam.path` 32 | - `db` option has been moved to `clam.db` 33 | - `scan_archives` option has been moved to `clam.scan_archives` 34 | - `scan_files` now supports directories as well and will obey your `scan_recursively` option. 35 | 36 | ## 0.6.1 (2015-01-05) 37 | 38 | - Updated description in package.json file. 39 | 40 | ## 0.6.2 (2015-01-05) 41 | 42 | - Fixed major bug in the scan_files method that was causing it to only scan half the files passed to it. 43 | 44 | ## 0.6.3 (2015-01-05) 45 | 46 | - Removed the unnecessary "index_old.js" file put there for reference during the 0.5.0 -> 0.6.0 semi-rewrite. 47 | 48 | ## 0.6.4 (2015-01-26) 49 | 50 | - Fixed error messages 51 | 52 | ## 0.7.0 (2015-06-01) 53 | 54 | - Fixed a bug caused by not passing a `file_cb` paramter to the `scan_file` method. Thanks nicolaspeixoto! 55 | - Added tests 56 | - Fixed poor validation of method parameters 57 | - Changed API of `scan_dir` such that the paramaters passed to the `end_cb` are different in certain defined situations. See the "NOTE" section of the `scan_dir` documentation for details. 58 | - Changed `err` paramter in all callbacks from a simple string to a proper javascript `Error` object. 59 | - Added documentation for how to use a file_list file for scanning. 60 | 61 | ## 0.7.1 (2015-06-05) 62 | 63 | - Added node dependency of > 0.12 to `package.json` file 64 | 65 | ## 0.8.0 (2015-06-05) 66 | 67 | - Removed item causing node > 0.12 dependency. 68 | - Removed dependency of node > 0.12 in `package.json` file. 69 | 70 | ## 0.8.1 (2015-06-09) 71 | 72 | - Fixed check for database file. Issue #6 73 | 74 | ## 0.8.2 (2015-08-14) 75 | 76 | - Updated to `execFile` instead of `exec` 77 | - Improved test suite 78 | 79 | ## 0.9.0-beta (2015-07-01) - Never Released 80 | 81 | - Added support for TCP/UNIX Domain socket communication to local or remote clamav services. 82 | - Added a `get_version` method. 83 | - NULL is now returned to the third parameter of the `is_infected` when file is neither infected or clean (i.e. on unexpected response) 84 | - Created alias: `scan_file` for `is_infected`. 85 | - Created a `scan_stream` method. 86 | - Minor code clean-up 87 | 88 | ## 1.0.0 (2019-05-02) 89 | 90 | This is a huge major release in which this module was essentially completely re-written. This version introduces some breaking changes and major new features. Please read the release notes below carefully. 91 | 92 | - Now requires at least Node v10.0.0 93 | - Code re-written in ES2018 code 94 | - Now supports a hybrid Promise/Callback API (supports async/await) 95 | - Now properly supports TCP/UNIX Domain socket communication to local or remote clamav services (with optional fallback to local binary via child process). 96 | - Added new `scan_stream` method which allows you to pass an input stream. 97 | - Added new `get_version` method which allows you to check the version of ClamAV that you'll be communicating with. 98 | - Added new `passthrough` method which allows you to pipe a stream "through" the clamscan module and on to another destination (ex. S3). 99 | - Added new alias `scan_file` that points to `is_infected`. 100 | - In order to provide the name of any viruses found, a new standard `viruses` array is now be provided to the callback for: 101 | 102 | - `is_infected` & `scan_file` methods (callback format: `(err, file, is_infected, viruses) => { ... }`). 103 | - `scan_files` method (callback format: `(err, good_files, bad_files, error_files, viruses) => { ... }`). 104 | - `scan_dir` method (callback format: `(err, good_files, bad_files, viruses) => { ... }`). 105 | 106 | - In all cases, the `viruses` parameter will be an empty array on error or when no viruses are found. 107 | 108 | - `scan_files` now has another additional parameter in its callback: 109 | 110 | - `error_files`: An object keyed by the filenames that presented errors while scanning. The value of those keys will be the error message for that file. 111 | 112 | - Introduces new API to instantiate the module (NOTE: The old way will no longer work! See below for more info). 113 | 114 | ### API Changes with 1.0.0: 115 | 116 | For some full-fledged examples of how the new API works, checkout the `/examples` directory in the module root directory. 117 | 118 | #### Module Initialization 119 | 120 | ##### Pre-1.0.0 121 | 122 | ```javascript 123 | const clamscan = require('clamscan')(options); 124 | ``` 125 | 126 | ##### 1.0.0 127 | 128 | **NOTE:** Due to the new asynchronous nature of the checks that are performed upon initialization of the module, the initialization method now returns a Promise instead of the actual instantiated object. Resolving the Promise with `then` will return the object like before. 129 | 130 | ```javascript 131 | const NodeClam = require('clamscan'); 132 | const ClamScan = new NodeClam().init(options); 133 | ``` 134 | 135 | #### Making Method Calls 136 | 137 | ##### Pre-1.0.0 138 | 139 | ```javascript 140 | clamscan.is_infected('/path/to/file.txt', (err, file, is_infected) => { 141 | // Do stuff 142 | }); 143 | ``` 144 | 145 | ##### 1.0.0 146 | 147 | ```javascript 148 | ClamScan.then(clamscan => { 149 | clamscan.is_infected('/path/to/file.txt', (err, file, is_infected, viruses) => { 150 | // Do stuff 151 | }); 152 | }); 153 | ``` 154 | 155 | If you prefer the async/await style of coding: 156 | 157 | ```javascript 158 | ;(async () => { 159 | const clamscan = await new NodeClam().init(options); 160 | clamscan.is_infected('/path/to/file.txt', (err, file, is_infected, viruses) => { 161 | // Do stuff 162 | }); 163 | })(); 164 | ``` 165 | 166 | #### New Way to Get Results 167 | 168 | ##### Pre-1.0.0 169 | 170 | The only way to get results/errors in pre-1.0.0 was through callbacks. 171 | 172 | ```javascript 173 | const clamscan = require('clamscan')(options); 174 | clamscan.scan_dir('/path/to/directory', (err, good_files, bad_files) => { 175 | // Do stuff inside callback 176 | }); 177 | ``` 178 | 179 | ##### 1.0.0 180 | 181 | In version 1.0.0 and beyond, you will now be able to use Promises as well (and, of course, async/await). 182 | 183 | ###### Promises 184 | 185 | ```javascript 186 | const ClamScan = new NodeClam().init(options); 187 | ClamScan.then(clamscan => 188 | clamscan.scan_dir('/path/to/directory').then(result => { 189 | const {good_files, bad_files} = result; 190 | // Do stuff 191 | }).catch(err => { 192 | // Handle scan error 193 | }); 194 | }).catch(err => { 195 | // Handle initialization error 196 | }); 197 | ``` 198 | 199 | ###### Async/Await 200 | 201 | ```javascript 202 | ;(async () => { 203 | try { 204 | const clamscan = await new NodeClam().init(options); 205 | const {good_files, bad_files} = await clamscan.scan_dir('/path/to/directory'); 206 | // Do stuff 207 | } catch (err) { 208 | // Handle any error 209 | } 210 | })(); 211 | ``` 212 | 213 | #### New Methods 214 | 215 | ##### scan_stream 216 | 217 | The `scan_stream` method allows you supply a readable stream to have it scanned. Theoretically any stream can be scanned this way. Like all methods, it supports callback and Promise response styles (full documentation is in README). 218 | 219 | ###### Basic Promise (async/await) Example: 220 | 221 | ```javascript 222 | ;(async () => { 223 | try { 224 | const clamscan = await new NodeClam().init(options); 225 | const stream = new Readable(); 226 | rs.push('foooooo'); 227 | rs.push('barrrrr'); 228 | rs.push(null); 229 | 230 | const {is_infected, viruses} = await clamscan.scan_stream(stream); 231 | 232 | // Do stuff 233 | } catch (err) { 234 | // Handle any error 235 | } 236 | })(); 237 | ``` 238 | 239 | ###### Basic Callback Example: 240 | 241 | ```javascript 242 | ;(async () => { 243 | try { 244 | const clamscan = await new NodeClam().init(options); 245 | const stream = new Readable(); 246 | rs.push('foooooo'); 247 | rs.push('barrrrr'); 248 | rs.push(null); 249 | 250 | clamscan.scan_stream(stream, (err, results) => { 251 | if (err) { 252 | // Handle error 253 | } else { 254 | const {is_infected, viruses} = results; 255 | // Do stuff 256 | } 257 | }); 258 | 259 | // Do stuff 260 | } catch (err) { 261 | // Handle any error 262 | } 263 | })(); 264 | ``` 265 | 266 | ##### passthrough 267 | 268 | The `passthrough` method allows you supply a readable stream that will be "passed-through" the clamscan module and onto another destination. In reality, the passthrough method works more like a fork stream whereby the input stream is simultaneously streamed to ClamAV and whatever is the next destination. Events are created when ClamAV is done and/or when viruses are detected so that you can decide what to do with the data on the next destination (delete if virus detected, for instance). Data is only passed through to the next generation if the data has been successfully received by ClamAV. If anything halts the data going to ClamAV (including issues caused by ClamAV), the entire pipeline is halted and events are fired. 269 | 270 | Normally, a file is uploaded and then scanned. This method should theoretically speed up user uploads intended to be scanned by up to 2x because the files are simultaneously scanned and written to disk. Your mileage my vary. 271 | 272 | This method is different than all the others in that it returns a PassthroughStream object and does not support a Promise or Callback API. This makes sense once you see the example below (full documentation is in README). 273 | 274 | ###### Basic Example: 275 | 276 | ```javascript 277 | ;(async () => { 278 | try { 279 | const clamscan = await new NodeClam().init(options); 280 | const request = require('request'); 281 | const input = request.get(some_url); 282 | const output = fs.createWriteStream(some_local_file); 283 | const av = clamscan.passthrough(); 284 | 285 | // Send output of RequestJS stream to ClamAV. 286 | // Send output of RequestJS to `some_local_file` if ClamAV receives data successfully 287 | input.pipe(av).pipe(output); 288 | 289 | // What happens when scan is completed 290 | av.on('scan-complete', result => { 291 | const {is_infected, viruses} = result; 292 | // Do stuff if you want 293 | }); 294 | 295 | // What happens when data has been fully written to `output` 296 | output.on('finish', () => { 297 | // Do stuff if you want 298 | }); 299 | } catch (err) { 300 | // Handle any error 301 | } 302 | })(); 303 | ``` 304 | 305 | ## 1.2.0 306 | 307 | ### SECURITY PATCH 308 | 309 | An important security patch was released in this version which fixes a bug causing false negatives in specific edge cases. Please upgrade immediately and only use this version from this point on. 310 | 311 | All older versions of this package have been deprecated on NPM. 312 | 313 | ## 1.3.0 314 | 315 | This just has some bug fixes and updates to dependencies. Technically, a new `'timeout'` event was added to the `passthrough` stream method, but, its not fully fleshed out and doesn't seem to work so it will remain undocumented for now. 316 | 317 | ## 1.4.0 318 | 319 | - Updated Mocha to v8.1.1. Subsequently, the oldest version of NodeJS allowed for this module is now v10.12.0. 320 | - Fixed issue with the method not throwing errors when testing existence and viability of remote/local socket. 321 | 322 | ## 1.4.1 323 | 324 | All sockets clients should now close when they are done being used, fail, or timeout. 325 | 326 | ## 1.4.2 327 | 328 | - Fixed initialization to pass a config-file option during clamav version check 329 | - Added new contributor 330 | - Fixed tests 331 | 332 | ## Newer Versions 333 | 334 | Please see the [GitHub Release page](https://github.com/kylefarris/clamscan/releases) for this project to see changelog info starting with v2.0.0. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Kyle Farris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | TESTS = tests/index.js 3 | 4 | all: 5 | @npm install 6 | 7 | test: all 8 | @mkdir -p tests/infected 9 | @mkdir -p tests/bad_scan_dir 10 | @mkdir -p tests/mixed_scan_dir/folder1 11 | @mkdir -p tests/mixed_scan_dir/folder2 12 | @touch tests/clamscan-log 13 | @./node_modules/.bin/mocha --exit --trace-warnings --trace-deprecation --retries 1 --full-trace --timeout 5000 --check-leaks --reporter spec $(TESTS) 14 | 15 | clean: 16 | rm -rf node_modules 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS Clamscan Virus Scanning Utility 2 | 3 | [![NPM Version][npm-version-image]][npm-url] [![NPM Downloads][npm-downloads-image]][npm-url] [![Node.js Version][node-image]][node-url] [![Test Suite](https://github.com/kylefarris/clamscan/actions/workflows/test.yml/badge.svg)](https://github.com/kylefarris/clamscan/actions/workflows/test.yml) 4 | 5 | Use Node JS to scan files on your server with ClamAV's clamscan/clamdscan binary or via TCP to a remote server or local UNIX Domain socket. This is especially useful for scanning uploaded files provided by un-trusted sources. 6 | 7 | # !!IMPORTANT 8 | 9 | If you are using a version prior to 1.2.0, please upgrade! There was a security vulnerability in previous versions that can cause false negative in some edge cases. Specific details on how the attack could be implemented will not be disclosed here. Please update to 1.2.0 or greater ASAP. No breaking changes are included, only the security patch. 10 | 11 | All older versions in NPM have been deprecated. 12 | 13 | # Version 1.0.0 Information 14 | 15 | If you are migrating from v0.8.5 or less to v1.0.0 or greater, please read the [release notes](https://github.com/kylefarris/clamscan/releases/tag/v1.0.0) as there are some breaking changes (but also some awesome new features!). 16 | 17 | # Table of Contents 18 | 19 | - [Dependencies](#dependencies) 20 | - [Local Binary Method](#to-use-local-binary-method-of-scanning) 21 | - [TCP/Domain Socket Method](#to-use-clamav-using-tcp-sockets) 22 | - [How to Install](#how-to-install) 23 | - [License Info](#license-info) 24 | - [Getting Started](#getting-started) 25 | - [A note about using this module via sockets or TCP](#a-note-about-using-this-module-via-sockets-or-tcp) 26 | - [Basic Usage Example](#basic-usage-example) 27 | - [API](#api) 28 | - [getVersion](#getVersion) 29 | - [isInfected (alias: scanFile)](#isInfected) 30 | - [scanDir](#scanDir) 31 | - [scanFiles](#scanFiles) 32 | - [scanStream](#scanStream) 33 | - [passthrough](#passthrough) 34 | - [ping](#ping) 35 | - [Contribute](#contribute) 36 | - [Resources used to help develop this module](#resources-used-to-help-develop-this-module) 37 | 38 | # Dependencies 39 | 40 | ## To use local binary method of scanning 41 | 42 | You will need to install ClamAV's clamscan binary and/or have clamdscan daemon running on your server. On linux, it's quite simple. 43 | 44 | Fedora-based distros: 45 | 46 | ```bash 47 | sudo yum install clamav 48 | ``` 49 | 50 | Debian-based distros: 51 | 52 | ```bash 53 | sudo apt-get install clamav clamav-daemon 54 | ``` 55 | 56 | For OS X, you can install clamav with brew: 57 | 58 | ```bash 59 | sudo brew install clamav 60 | ``` 61 | 62 | ## To use ClamAV using TCP sockets 63 | 64 | You will need access to either: 65 | 66 | 1. A local UNIX Domain socket for a local instance of `clamd` 67 | 68 | - Follow instructions in [To use local binary method of scanning](#user-content-to-use-local-binary-method-of-scanning). 69 | - Socket file is usually: `/var/run/clamd.scan/clamd.sock` 70 | - Make sure `clamd` is running on your local server 71 | 72 | 2. A local/remote `clamd` daemon 73 | 74 | - Must know the port the daemon is running on 75 | - If running on remote server, you must have the IP address/domain name 76 | - If running on remote server, it's firewall must have the appropriate TCP port(s) open 77 | - Make sure `clamd` is running on your local/remote server 78 | 79 | **NOTE:** This module is not intended to work on a Windows server. This would be a welcome addition if someone wants to add that feature (I may get around to it one day but have no urgent need for this). 80 | 81 | # How to Install 82 | 83 | ```bash 84 | npm install clamscan 85 | ``` 86 | 87 | # License Info 88 | 89 | Licensed under the MIT License: 90 | 91 | - 92 | 93 | # Getting Started 94 | 95 | All of the values listed in the example below represent the default values for their respective configuration item. 96 | 97 | You can simply do this: 98 | 99 | ```javascript 100 | const NodeClam = require('clamscan'); 101 | const ClamScan = new NodeClam().init(); 102 | ``` 103 | 104 | And, you'll be good to go. 105 | 106 | **BUT**: If you want more control, you can specify all sorts of options. 107 | 108 | ```javascript 109 | const NodeClam = require('clamscan'); 110 | const ClamScan = new NodeClam().init({ 111 | removeInfected: false, // If true, removes infected files 112 | quarantineInfected: false, // False: Don't quarantine, Path: Moves files to this place. 113 | scanLog: null, // Path to a writeable log file to write scan results into 114 | debugMode: false, // Whether or not to log info/debug/error msgs to the console 115 | fileList: null, // path to file containing list of files to scan (for scanFiles method) 116 | scanRecursively: true, // If true, deep scan folders recursively 117 | clamscan: { 118 | path: '/usr/bin/clamscan', // Path to clamscan binary on your server 119 | db: null, // Path to a custom virus definition database 120 | scanArchives: true, // If true, scan archives (ex. zip, rar, tar, dmg, iso, etc...) 121 | active: true // If true, this module will consider using the clamscan binary 122 | }, 123 | clamdscan: { 124 | socket: false, // Socket file for connecting via TCP 125 | host: false, // IP of host to connect to TCP interface 126 | port: false, // Port of host to use when connecting via TCP interface 127 | timeout: 60000, // Timeout for scanning files 128 | localFallback: true, // Use local preferred binary to scan if socket/tcp fails 129 | path: '/usr/bin/clamdscan', // Path to the clamdscan binary on your server 130 | configFile: null, // Specify config file if it's in an unusual place 131 | multiscan: true, // Scan using all available cores! Yay! 132 | reloadDb: false, // If true, will re-load the DB on every call (slow) 133 | active: true, // If true, this module will consider using the clamdscan binary 134 | bypassTest: false, // Check to see if socket is available when applicable 135 | tls: false, // Use plaintext TCP to connect to clamd 136 | }, 137 | preference: 'clamdscan' // If clamdscan is found and active, it will be used by default 138 | }); 139 | ``` 140 | 141 | Here is a _non-default values example_ (to help you get an idea of what proper-looking values could be): 142 | 143 | ```javascript 144 | const NodeClam = require('clamscan'); 145 | const ClamScan = new NodeClam().init({ 146 | removeInfected: true, // Removes files if they are infected 147 | quarantineInfected: '~/infected/', // Move file here. removeInfected must be FALSE, though. 148 | scanLog: '/var/log/node-clam', // You're a detail-oriented security professional. 149 | debugMode: true, // This will put some debug info in your js console 150 | fileList: '/home/webuser/scanFiles.txt', // path to file containing list of files to scan 151 | scanRecursively: false, // Choosing false here will save some CPU cycles 152 | clamscan: { 153 | path: '/usr/bin/clam', // I dunno, maybe your clamscan is just call "clam" 154 | scanArchives: false, // Choosing false here will save some CPU cycles 155 | db: '/usr/bin/better_clam_db', // Path to a custom virus definition database 156 | active: false // you don't want to use this at all because it's evil 157 | }, 158 | clamdscan: { 159 | socket: '/var/run/clamd.scan/clamd.sock', // This is pretty typical 160 | host: '127.0.0.1', // If you want to connect locally but not through socket 161 | port: 12345, // Because, why not 162 | timeout: 300000, // 5 minutes 163 | localFallback: false, // Do no fail over to binary-method of scanning 164 | path: '/bin/clamdscan', // Special path to the clamdscan binary on your server 165 | configFile: '/etc/clamd.d/daemon.conf', // A fairly typical config location 166 | multiscan: false, // You hate speed and multi-threaded awesome-sauce 167 | reloadDb: true, // You want your scans to run slow like with clamscan 168 | active: false, // you don't want to use this at all because it's evil 169 | bypassTest: true, // Don't check to see if socket is available. You should probably never set this to true. 170 | tls: true, // Connect to clamd over TLS 171 | }, 172 | preference: 'clamscan' // If clamscan is found and active, it will be used by default 173 | }); 174 | ``` 175 | 176 | NOTE: If a valid `port` is provided but no `host` value is provided, the clamscan will assume `'localhost'` for `host`. 177 | 178 | ## A note about using this module via sockets or TCP 179 | 180 | As of version v1.0.0, this module supports communication with a local or remote ClamAV daemon through Unix Domain sockets or a TCP host/port combo. If you supply both in your configuration object, the UNIX Domain socket option will be used. The module _will not_ not fallback to using the alternative Host/Port method. If you wish to connect via Host/Port and not a Socket, please either omit the `socket` property in the config object or use `socket: null`. 181 | 182 | If you specify a valid clamscan/clamdscan binary in your config and you set `clamdscan.localFallback: true` in your config, this module _will_ fallback to the traditional way this module has worked--using a binary directly/locally. 183 | 184 | Also, there are some caveats to using the socket/tcp based approach: 185 | 186 | - The following configuration items are not honored (unless the module falls back to binary method): 187 | 188 | - `removeInfected` - remote clamd service config will dictate this 189 | - `quarantineInfected` - remote clamd service config will dictate this 190 | - `scanLog` - remote clamd service config will dictate this 191 | - `fileList` - this simply won't be available 192 | - `clamscan.db` - only available on fallback 193 | - `clamscan.scanArchives` - only available on fallback 194 | - `clamscan.path` - only available on fallback 195 | - `clamdscan.configFile` - only available on fallback 196 | - `clamdscan.path` - only available on fallback 197 | 198 | # Basic Usage Example 199 | 200 | For the sake of brevity, all the examples in the [API](#api) section will be shortened to just the relevant parts related specifically to that example. In those examples, we'll assume you already have an instance of the `clamscan` object. Since initializing the module returns a promise, you'll have to resolve that promise to get an instance of the `clamscan` object. 201 | 202 | **Below is the _full_ example of how you could get that instance and run some methods:** 203 | 204 | ```javascript 205 | const NodeClam = require('clamscan'); 206 | const ClamScan = new NodeClam().init(options); 207 | 208 | // Get instance by resolving ClamScan promise object 209 | ClamScan.then(async clamscan => { 210 | try { 211 | // You can re-use the `clamscan` object as many times as you want 212 | const version = await clamscan.getVersion(); 213 | console.log(`ClamAV Version: ${version}`); 214 | 215 | const {isInfected, file, viruses} = await clamscan.isInfected('/some/file.zip'); 216 | if (isInfected) console.log(`${file} is infected with ${viruses}!`); 217 | } catch (err) { 218 | // Handle any errors raised by the code in the try block 219 | } 220 | }).catch(err => { 221 | // Handle errors that may have occurred during initialization 222 | }); 223 | ``` 224 | 225 | **If you're writing your code within an async function, getting an instance can be one less step:** 226 | 227 | ```javascript 228 | const NodeClam = require('clamscan'); 229 | 230 | async some_function() { 231 | try { 232 | // Get instance by resolving ClamScan promise object 233 | const clamscan = await new NodeClam().init(options); 234 | const {goodFiles, badFiles} = await clamscan.scanDir('/foo/bar'); 235 | } catch (err) { 236 | // Handle any errors raised by the code in the try block 237 | } 238 | } 239 | 240 | some_function(); 241 | ``` 242 | 243 | # API 244 | 245 | Complete/functional examples for various use-cases can be found in the [examples folder](https://github.com/kylefarris/clamscan/tree/master/examples). 246 | 247 | 248 | 249 | ## .getVersion([callback]) 250 | 251 | This method allows you to determine the version of ClamAV you are interfacing with. It supports a callback and Promise API. If no callback is supplied, a Promise will be returned. 252 | 253 | ### Parameters 254 | 255 | - `callback` (function) (optional) Will be called when the scan is complete. It receives 2 parameters: 256 | 257 | - `err` (object or null) A standard javascript Error object (null if no error) 258 | - `version` (string) The version of the clamav server you're interfacing with 259 | 260 | ### Returns 261 | 262 | - Promise 263 | 264 | - Promise resolution returns: `version` (string) The version of the clamav server you're interfacing with 265 | 266 | ### Callback Example 267 | 268 | ```javascript 269 | clamscan.getVersion((err, version) => { 270 | if (err) return console.error(err); 271 | console.log(`ClamAV Version: ${version}`); 272 | }); 273 | ``` 274 | 275 | ### Promise Example 276 | 277 | ```javascript 278 | clamscan.getVersion().then(version => { 279 | console.log(`ClamAV Version: ${version}`); 280 | }).catch(err => { 281 | console.error(err); 282 | }); 283 | ``` 284 | 285 | 286 | 287 | ## .isInfected(filePath[,callback]) 288 | 289 | This method allows you to scan a single file. It supports a callback and Promise API. If no callback is supplied, a Promise will be returned. This method will likely be the most common use-case for this module. 290 | 291 | ### Alias 292 | 293 | `.scan_file` 294 | 295 | ### Parameters 296 | 297 | - `filePath` (string) Represents a path to the file to be scanned. 298 | - `callback` (function) (optional) Will be called when the scan is complete. It takes 3 parameters: 299 | 300 | - `err` (object or null) A standard javascript Error object (null if no error) 301 | - `file` (string) The original `filePath` passed into the `isInfected` method. 302 | - `isInfected` (boolean) **True**: File is infected; **False**: File is clean. **NULL**: Unable to scan. 303 | - `viruses` (array) An array of any viruses found in the scanned file. 304 | 305 | ### Returns 306 | 307 | - Promise 308 | 309 | - Promise resolution returns: `result` (object): 310 | 311 | - `file` (string) The original `filePath` passed into the `isInfected` method. 312 | - `isInfected` (boolean) **True**: File is infected; **False**: File is clean. **NULL**: Unable to scan. 313 | - `viruses` (array) An array of any viruses found in the scanned file. 314 | 315 | ### Callback Example 316 | 317 | ```javascript 318 | clamscan.isInfected('/a/picture/for_example.jpg', (err, file, isInfected, viruses) => { 319 | if (err) return console.error(err); 320 | 321 | if (isInfected) { 322 | console.log(`${file} is infected with ${viruses.join(', ')}.`); 323 | } 324 | }); 325 | ``` 326 | 327 | ### Promise Example 328 | 329 | ```javascript 330 | clamscan.isInfected('/a/picture/for_example.jpg').then(result => { 331 | const {file, isInfected, viruses} = result; 332 | if (isInfected) console.log(`${file} is infected with ${viruses.join(', ')}.`); 333 | }).then(err => { 334 | console.error(err); 335 | }) 336 | ``` 337 | 338 | ### Async/Await Example 339 | 340 | ```javascript 341 | const {file, isInfected, viruses} = await clamscan.isInfected('/a/picture/for_example.jpg'); 342 | ``` 343 | 344 | 345 | 346 | ## .scanDir(dirPath[,endCallback[,fileCallback]]) 347 | 348 | Allows you to scan an entire directory for infected files. This obeys your `recursive` option even for `clamdscan` which does not have a native way to turn this feature off. If you have multiple paths, send them in an array to `scanFiles`. 349 | 350 | **TL;DR:** For maximum speed, don't supply a `fileCallback`. 351 | 352 | If you choose to supply a `fileCallback`, the scan will run a little bit slower (depending on number of files to be scanned) for `clamdscan`. If you are using `clamscan`, while it will work, I'd highly advise you to NOT pass a `fileCallback`... it will run incredibly slow. 353 | 354 | ### NOTE 355 | 356 | The `goodFiles` parameter of the `endCallback` callback in this method will only contain the directory that was scanned in **all** **but** the following scenarios: 357 | 358 | - A `fileCallback` callback is provided, and `scanRecursively` is set to _true_. 359 | - The scanner is set to `clamdscan` and `scanRecursively` is set to _false_. 360 | - The scanned directory contains 1 or more viruses. In this case, the `goodFiles` array will be empty. 361 | 362 | There will, however, be a total count of the good files which is calculated by determining the total number of files scanned and subtracting the number of bad files from that count. We simply can't provide a list of all good files due to the potential large memory usage implications of scanning a directory with, for example, _millions_ of files. 363 | 364 | ### Parameters 365 | 366 | - `dirPath` (string) (required) Full path to the directory to scan. 367 | - `endCallback` (function) (optional) Will be called when the entire directory has been completely scanned. This callback takes 3 parameters: 368 | 369 | - `err` (object) A standard javascript Error object (null if no error) 370 | - `goodFiles` (array) An *empty* array if path is _infected_. An array containing the directory name that was passed in if _clean_. 371 | - `badFiles` (array) List of the full paths to all files that are _infected_. 372 | - `viruses` (array) List of all the viruses found (feature request: associate to the bad files). 373 | - `numGoodFiles` (number) Number of files that were found to be clean. 374 | 375 | - `fileCallback` (function) (optional) Will be called after each file in the directory has been scanned. This is useful for keeping track of the progress of the scan. This callback takes 3 parameters: 376 | 377 | - `err` (object or null) A standard Javascript Error object (null if no error) 378 | - `file` (string) Path to the file that just got scanned. 379 | - `isInfected` (boolean) **True**: File is infected; **False**: File is clean. **NULL**: Unable to scan file. 380 | 381 | ### Returns 382 | 383 | - Promise 384 | 385 | - Promise resolution returns: `result` (object): 386 | 387 | - `path` (string) The original `dir_path` passed into the `scanDir` method. 388 | - `isInfected` (boolean) **True**: File is infected; **False**: File is clean. **NULL**: Unable to scan. 389 | - `goodFiles` (array) An *empty* array if path is _infected_. An array containing the directory name that was passed in if _clean_. 390 | - `badFiles` (array) List of the full paths to all files that are _infected_. 391 | - `viruses` (array) List of all the viruses found (feature request: associate to the bad files). 392 | - `numGoodFiles` (number) Number of files that were found to be clean. 393 | 394 | ### Callback Example 395 | 396 | ```javascript 397 | clamscan.scanDir('/some/path/to/scan', (err, goodFiles, badFiles, viruses, numGoodFiles) { 398 | if (err) return console.error(err); 399 | 400 | if (badFiles.length > 0) { 401 | console.log(`${path} was infected. The offending files (${badFiles.join (', ')}) have been quarantined.`); 402 | console.log(`Viruses Found: ${viruses.join(', ')}`); 403 | } else { 404 | console.log(`${goodFiles[0]} looks good! ${numGoodFiles} file scanned and no problems found!.`); 405 | } 406 | }); 407 | ``` 408 | 409 | ### Promise Example 410 | 411 | ```javascript 412 | clamscan.scanDir('/some/path/to/scan').then(results => { 413 | const { path, isInfected, goodFiles, badFiles, viruses, numGoodFiles } = results; 414 | //... 415 | }).catch(err => { 416 | return console.error(err); 417 | }); 418 | ``` 419 | 420 | ### Async/Await Example 421 | 422 | ```javascript 423 | const { path, isInfected, goodFiles, badFiles, viruses, numGoodFiles } = await clamscan.scanDir('/some/path/to/scan'); 424 | ``` 425 | 426 | 427 | 428 | ## .scanFiles(files[,endCallback[,fileCallback]]) 429 | 430 | This allows you to scan many files that might be in different directories or maybe only certain files of a single directory. This is essentially a wrapper for `isInfected` that simplifies the process of scanning many files or directories. 431 | 432 | ### Parameters 433 | 434 | - `files` (array) (optional) A list of strings representing full paths to files you want scanned. If not supplied, the module will check for a `fileList` config option. If neither is found, the method will throw an error. 435 | - `endCallback` (function) (optional) Will be called when the entire list of files has been completely scanned. This callback takes 3 parameters: 436 | 437 | - `err` (object or null) A standard JavaScript Error object (null if no error) 438 | - `goodFiles` (array) List of the full paths to all files that are _clean_. 439 | - `badFiles` (array) List of the full paths to all files that are _infected_. 440 | 441 | - `fileCallback` (function) (optional) Will be called after each file in the list has been scanned. This is useful for keeping track of the progress of the scan. This callback takes 3 parameters: 442 | 443 | - `err` (object or null) A standard JavaScript Error object (null if no error) 444 | - `file` (string) Path to the file that just got scanned. 445 | - `isInfected` (boolean) **True**: File is infected; **False**: File is clean. **NULL**: Unable to scan file. 446 | 447 | ### Returns 448 | 449 | - Promise 450 | 451 | - Promise resolution returns: `result` (object): 452 | 453 | - `goodFiles` (array) List of the full paths to all files that are _clean_. 454 | - `badFiles` (array) List of the full paths to all files that are _infected_. 455 | - `errors` (object) Per-file errors keyed by the filename in which the error happened. (ex. `{'foo.txt': Error}`) 456 | - `viruses` (array) List of all the viruses found (feature request: associate to the bad files). 457 | 458 | ### Callback Example 459 | 460 | ```javascript 461 | const scan_status = { good: 0, bad: 0 }; 462 | const files = [ 463 | '/path/to/file/1.jpg', 464 | '/path/to/file/2.mov', 465 | '/path/to/file/3.rb' 466 | ]; 467 | clamscan.scanFiles(files, (err, goodFiles, badFiles, viruses) => { 468 | if (err) return console.error(err); 469 | if (badFiles.length > 0) { 470 | console.log({ 471 | msg: `${goodFiles.length} files were OK. ${badFiles.length} were infected!`, 472 | badFiles, 473 | goodFiles, 474 | viruses, 475 | }); 476 | } else { 477 | res.send({msg: "Everything looks good! No problems here!."}); 478 | } 479 | }, (err, file, isInfected, viruses) => { 480 | ;(isInfected ? scan_status.bad++ : scan_status.good++); 481 | console.log(`${file} is ${(isInfected ? `infected with ${viruses}` : 'ok')}.`); 482 | console.log('Scan Status: ', `${(scan_status.bad + scan_status.good)}/${files.length}`); 483 | }); 484 | ``` 485 | 486 | ### Promise Example 487 | 488 | **Note:** There is currently no way to get per-file notifications with the Promise API. 489 | 490 | ```javascript 491 | clamscan.scanFiles(files).then(results => { 492 | const { goodFiles, badFiles, errors, viruses } = results; 493 | // ... 494 | }).catch(err => { 495 | console.error(err); 496 | }) 497 | ``` 498 | 499 | ### Async/Await Example 500 | 501 | ```javascript 502 | const { goodFiles, badFiles, errors, viruses } = await clamscan.scanFiles(files); 503 | ``` 504 | 505 | #### Scanning files listed in fileList 506 | 507 | If this modules is configured with a valid path to a file containing a newline-delimited list of files, it will use the list in that file when scanning if the first paramter passed is falsy. 508 | 509 | **Files List Document:** 510 | 511 | ```bash 512 | /some/path/to/file.zip 513 | /some/other/path/to/file.exe 514 | /one/more/file/to/scan.rb 515 | ``` 516 | 517 | **Script:** 518 | 519 | ```javascript 520 | const ClamScan = new NodeClam().init({ 521 | fileList: '/path/to/fileList.txt' 522 | }); 523 | 524 | ClamScan.then(async clamscan => { 525 | // Supply nothing to first parameter to use `fileList` 526 | const { goodFiles, badFiles, errors, viruses } = await clamscan.scanFiles(); 527 | }); 528 | ``` 529 | 530 | 531 | 532 | ## .scanStream(stream[,callback]) 533 | 534 | This method allows you to scan a binary stream. **NOTE**: This method will only work if you've configured the module to allow the use of a TCP or UNIX Domain socket. In other words, this will not work if you only have access to a local ClamAV binary. 535 | 536 | ### Parameters 537 | 538 | - `stream` (stream) A readable stream object 539 | - `callback` (function) (optional) Will be called after the stream has been scanned (or attempted to be scanned): 540 | 541 | - `err` (object or null) A standard JavaScript Error object (null if no error) 542 | - `isInfected` (boolean) **True**: Stream is infected; **False**: Stream is clean. **NULL**: Unable to scan file. 543 | 544 | ### Returns 545 | 546 | - Promise 547 | 548 | - Promise resolution returns: `result` (object): 549 | 550 | - `file` (string) **NULL** as no file path can be provided with the stream 551 | - `isInfected` (boolean) **True**: File is infected; **False**: File is clean. **NULL**: Unable to scan. 552 | - `viruses` (array) An array of any viruses found in the scanned file. 553 | 554 | ### Examples 555 | 556 | **Callback Example:** 557 | 558 | ```javascript 559 | const NodeClam = require('clamscan'); 560 | 561 | // You'll need to specify your socket or TCP connection info 562 | const clamscan = new NodeClam().init({ 563 | clamdscan: { 564 | socket: '/var/run/clamd.scan/clamd.sock', 565 | host: '127.0.0.1', 566 | port: 3310, 567 | } 568 | }); 569 | const Readable = require('stream').Readable; 570 | const rs = Readable(); 571 | 572 | rs.push('foooooo'); 573 | rs.push('barrrrr'); 574 | rs.push(null); 575 | 576 | clamscan.scanStream(stream, (err, { isInfected. viruses }) => { 577 | if (err) return console.error(err); 578 | if (isInfected) return console.log('Stream is infected! Booo!', viruses); 579 | console.log('Stream is not infected! Yay!'); 580 | }); 581 | ``` 582 | 583 | **Promise Example:** 584 | 585 | ```javascript 586 | clamscan.scanStream(stream).then(({isInfected}) => { 587 | if (isInfected) return console.log("Stream is infected! Booo!"); 588 | console.log("Stream is not infected! Yay!"); 589 | }).catch(err => { 590 | console.error(err); 591 | }; 592 | ``` 593 | 594 | **Promise Example:** 595 | 596 | ```javascript 597 | const { isInfected, viruses } = await clamscan.scanStream(stream); 598 | ``` 599 | 600 | 601 | 602 | ## .passthrough() 603 | 604 | The `passthrough` method returns a PassthroughStream object which allows you pipe a ReadbleStream through it and on to another output. In the case of this module's passthrough implementation, it's actually forking the data to also go to ClamAV via TCP or Domain Sockets. Each data chunk is only passed on to the output if that chunk was successfully sent to and received by ClamAV. The PassthroughStream object returned from this method has a special event that is emitted when ClamAV finishes scanning the streamed data so that you can decide if there's anything you need to do with the final output destination (ex. delete a file or S3 object). 605 | 606 | In typical, non-passthrough setups, a file is uploaded to the local filesytem and then subsequently scanned. With that setup, you have to wait for the upload to complete _and then wait again_ for the scan to complete. Using this module's `passthrough` method, you could theoretically speed up user uploads intended to be scanned by up to 2x because the files are simultaneously scanned and written to any WriteableStream output (examples: filesystem, S3, gzip, etc...). 607 | 608 | As for these theoretical gains, your mileage my vary and I'd love to hear feedback on this to see where things can still be improved. 609 | 610 | Please note that this method is different than all the others in that it returns a PassthroughStream object and does not support a Promise or Callback API. This makes sense once you see the example below (a practical working example can be found in the examples directory of this module): 611 | 612 | ### Example 613 | 614 | ```javascript 615 | const NodeClam = require('clamscan'); 616 | 617 | // You'll need to specify your socket or TCP connection info 618 | const clamscan = new NodeClam().init({ 619 | clamdscan: { 620 | socket: '/var/run/clamd.scan/clamd.sock', 621 | host: '127.0.0.1', 622 | port: 3310, 623 | } 624 | }); 625 | 626 | // For example's sake, we're using the Axios module 627 | const axios = require('Axios'); 628 | 629 | // Get a readable stream for a URL request 630 | const input = axios.get(some_url); 631 | 632 | // Create a writable stream to a local file 633 | const output = fs.createWriteStream(some_local_file); 634 | 635 | // Get instance of this module's PassthroughStream object 636 | const av = clamscan.passthrough(); 637 | 638 | // Send output of Axios stream to ClamAV. 639 | // Send output of Axios to `some_local_file` if ClamAV receives data successfully 640 | input.pipe(av).pipe(output); 641 | 642 | // What happens when scan is completed 643 | av.on('scan-complete', result => { 644 | const { isInfected, viruses } = result; 645 | // Do stuff if you want 646 | }); 647 | 648 | // What happens when data has been fully written to `output` 649 | output.on('finish', () => { 650 | // Do stuff if you want 651 | }); 652 | 653 | // NOTE: no errors (or other events) are being handled in this example but standard errors will be emitted according to NodeJS's Stream specifications 654 | ``` 655 | 656 | 657 | 658 | ## .ping() 659 | 660 | This method checks to see if the remote/local socket is working. It supports a callback and Promise API. If no callback is supplied, a Promise will be returned. This method can be used for healthcheck purposes and is already implicitly used during scan. 661 | 662 | ### Parameters 663 | 664 | - `callback` (function) (optional) Will be called after the ping: 665 | 666 | - `err` (object or null) A standard JavaScript Error object (null if no error) 667 | - `client` (object) A copy of the Socket/TCP client 668 | 669 | ### Returns 670 | 671 | - Promise 672 | 673 | - Promise resolution returns: `client` (object): A copy of the Socket/TCP client 674 | 675 | ### Examples 676 | 677 | **Callback Example:** 678 | 679 | ```javascript 680 | const NodeClam = require('clamscan'); 681 | 682 | // You'll need to specify your socket or TCP connection info 683 | const clamscan = new NodeClam().init({ 684 | clamdscan: { 685 | socket: '/var/run/clamd.scan/clamd.sock', 686 | host: '127.0.0.1', 687 | port: 3310, 688 | } 689 | }); 690 | 691 | clamscan.ping((err, client) => { 692 | if (err) return console.error(err); 693 | console.log('ClamAV is still working!'); 694 | client.end(); 695 | }); 696 | ``` 697 | 698 | **Promise Example:** 699 | 700 | ```javascript 701 | clamscan.ping().then((client) => { 702 | console.log('ClamAV is still working!'); 703 | client.end(); 704 | }).catch(err => { 705 | console.error(err); 706 | }; 707 | ``` 708 | 709 | **Promise Example:** 710 | 711 | ```javascript 712 | const client = await clamscan.ping(); 713 | client.end(); 714 | ``` 715 | 716 | # Contribute 717 | 718 | Got a missing feature you'd like to use? Found a bug? Go ahead and fork this repo, build the feature and issue a pull request. 719 | 720 | # Resources used to help develop this module 721 | 722 | - 723 | - 724 | - 725 | - 726 | - 727 | 728 | [node-image]: https://img.shields.io/node/v/clamscan.svg 729 | [node-url]: https://nodejs.org/en/download 730 | [npm-downloads-image]: https://img.shields.io/npm/dm/clamscan.svg 731 | [npm-url]: https://npmjs.org/package/clamscan 732 | [npm-version-image]: https://img.shields.io/npm/v/clamscan.svg 733 | [travis-image]: https://img.shields.io/travis/kylefarris/clamscan/master.svg 734 | [travis-url]: https://travis-ci.org/kylefarris/clamscan 735 | -------------------------------------------------------------------------------- /examples/basic_async_await.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const axios = require('axios'); 3 | const fs = require('fs'); 4 | 5 | const fakeVirusUrl = 'https://secure.eicar.org/eicar_com.txt'; 6 | const tempDir = __dirname; 7 | const scanFile = `${tempDir}/tmp_file.txt`; 8 | 9 | const config = { 10 | removeInfected: true, 11 | debugMode: false, 12 | scanRecursively: false, 13 | clamdscan: { 14 | path: '/usr/bin/clamdscan', 15 | // config_file: '/etc/clamd.d/daemon.conf' 16 | }, 17 | preference: 'clamdscan', 18 | }; 19 | 20 | // Initialize the clamscan module 21 | const NodeClam = require('../index'); // Offically: require('clamscan'); 22 | 23 | (async () => { 24 | const clamscan = await new NodeClam().init(config); 25 | let body; 26 | 27 | // Request a test file from the internet... 28 | try { 29 | body = await axios.get(fakeVirusUrl); 30 | } catch (err) { 31 | if (err.response) console.err(`${err.response.status}: Request Failed. `, err.response.data); 32 | else if (err.request) console.error('Error with Request: ', err.request); 33 | else console.error('Error: ', err.message); 34 | process.exit(1); 35 | } 36 | 37 | // Write the file to the filesystem 38 | fs.writeFileSync(scanFile, body); 39 | 40 | // Scan the file 41 | try { 42 | const { file, isInfected, viruses } = await clamscan.isInfected(scanFile); 43 | 44 | // If `isInfected` is TRUE, file is a virus! 45 | if (isInfected === true) { 46 | console.log( 47 | `You've downloaded a virus (${viruses.join( 48 | '' 49 | )})! Don't worry, it's only a test one and is not malicious...` 50 | ); 51 | } else if (isInfected === null) { 52 | console.log("Something didn't work right..."); 53 | } else if (isInfected === false) { 54 | console.log(`The file (${file}) you downloaded was just fine... Carry on...`); 55 | } 56 | 57 | // Remove the file (for good measure) 58 | if (fs.existsSync(scanFile)) fs.unlinkSync(scanFile); 59 | process.exit(0); 60 | } catch (err) { 61 | console.error(`ERROR: ${err}`); 62 | console.trace(err.stack); 63 | process.exit(1); 64 | } 65 | })(); 66 | -------------------------------------------------------------------------------- /examples/passthrough.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const axios = require('axios'); 3 | const fs = require('fs'); 4 | const { promisify } = require('util'); 5 | 6 | const fsUnlink = promisify(fs.unlink); 7 | 8 | // const fakeVirusUrl = 'https://secure.eicar.org/eicar_com.txt'; 9 | const normalFileUrl = 'https://raw.githubusercontent.com/kylefarris/clamscan/sockets/README.md'; 10 | // const largeFileUrl = 'http://speedtest-ny.turnkeyinternet.net/100mb.bin'; 11 | const passthruFile = `${__dirname}/output`; 12 | 13 | const testUrl = normalFileUrl; 14 | // const testUrl = fakeVirusUrl; 15 | // const testUrl = largeFileUrl; 16 | 17 | // Initialize the clamscan module 18 | const NodeClam = require('../index'); // Offically: require('clamscan'); 19 | 20 | /** 21 | * Removes whatever file was passed-through during the scan. 22 | */ 23 | async function removeFinalFile() { 24 | try { 25 | await fsUnlink(passthruFile); 26 | console.log(`Output file: "${passthruFile}" was deleted.`); 27 | process.exit(1); 28 | } catch (err) { 29 | console.error(err); 30 | process.exit(1); 31 | } 32 | } 33 | 34 | /** 35 | * Actually run the example code. 36 | */ 37 | async function test() { 38 | const clamscan = await new NodeClam().init({ 39 | debugMode: true, 40 | clamdscan: { 41 | host: 'localhost', 42 | port: 3310, 43 | bypassTest: true, 44 | // socket: '/var/run/clamd.scan/clamd.sock', 45 | }, 46 | }); 47 | 48 | const input = axios.get(testUrl); 49 | const output = fs.createWriteStream(passthruFile); 50 | const av = clamscan.passthrough(); 51 | 52 | input.pipe(av).pipe(output); 53 | 54 | av.on('error', (error) => { 55 | if ('data' in error && error.data.isInfected) { 56 | console.error('Dang, your stream contained a virus(es):', error.data.viruses); 57 | } else { 58 | console.error(error); 59 | } 60 | removeFinalFile(); 61 | }) 62 | .on('timeout', () => { 63 | console.error('It looks like the scanning has timedout.'); 64 | process.exit(1); 65 | }) 66 | .on('finish', () => { 67 | console.log('All data has been sent to virus scanner'); 68 | }) 69 | .on('end', () => { 70 | console.log('All data has been scanned sent on to the destination!'); 71 | }) 72 | .on('scan-complete', (result) => { 73 | console.log('Scan Complete: Result: ', result); 74 | if (result.isInfected === true) { 75 | console.log( 76 | `You've downloaded a virus (${result.viruses.join( 77 | ', ' 78 | )})! Don't worry, it's only a test one and is not malicious...` 79 | ); 80 | } else if (result.isInfected === null) { 81 | console.log(`There was an issue scanning the file you downloaded...`); 82 | } else { 83 | console.log(`The file (${testUrl}) you downloaded was just fine... Carry on...`); 84 | } 85 | removeFinalFile(); 86 | process.exit(0); 87 | }); 88 | 89 | output.on('finish', () => { 90 | console.log('Data has been fully written to the output...'); 91 | output.destroy(); 92 | }); 93 | 94 | output.on('error', (error) => { 95 | console.log('Final Output Fail: ', error); 96 | process.exit(1); 97 | }); 98 | } 99 | 100 | test(); 101 | -------------------------------------------------------------------------------- /examples/pipe2s3.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/no-unresolved */ 4 | const EventEmitter = require('events'); 5 | const filesize = require('filesize'); 6 | const { uuidv4 } = require('uuid'); 7 | const NodeClam = require('clamscan'); 8 | const BusBoy = require('busboy'); 9 | const AWS = require('aws-sdk'); 10 | 11 | AWS.config.region = ''; 12 | 13 | const ClamScan = new NodeClam().init({ 14 | removeInfected: true, 15 | scanRecursively: false, 16 | clamdscan: { 17 | socket: '/var/run/clamd.scan/clamd.sock', 18 | timeout: 300000, 19 | localFallback: true, 20 | }, 21 | preference: 'clamdscan', 22 | }); 23 | 24 | const s3Config = { 25 | params: { 26 | Bucket: '', 27 | }, 28 | }; 29 | const s3 = new AWS.S3(s3Config); 30 | const s3Stream = require('s3-upload-stream')(s3); 31 | 32 | /** 33 | * Example method for taking an end-user's upload stream and piping it though 34 | * clamscan and then on to S3 with full error-handling. This method assumes 35 | * you're using ExpressJS as your server. 36 | * 37 | * NOTE: This method can only handle one file in a request payload. 38 | * 39 | * @param {object} req - An Express Request object 40 | * @param {object} res - An Express Response object 41 | * @param {object} [opts] - Used to override defaults 42 | * @returns {Promise} Object like: { s3Details, fileInfo, fields } 43 | */ 44 | async function pipe2s3(req, res, opts = {}) { 45 | let debugMode = false; 46 | const pipeline = new EventEmitter(); 47 | 48 | return new Promise((resolve, reject) => { 49 | let s3Details = null; 50 | let scanResult = null; 51 | const fileInfo = {}; 52 | const fields = {}; 53 | let numFiles = 0; 54 | let s3UploadStream; 55 | 56 | const defaults = { 57 | s3_path: '', // Needs trailing slash if provided... 58 | s3Id: null, 59 | s3_acl: 'private', 60 | s3_metadata: {}, 61 | max_file_size: 10 * 1024 ** 2, // 20 MB 62 | max_files: null, // FALSEY === No max number of files 63 | allowed_mimetypes: [], // FALSEY === Accept anything 64 | }; 65 | 66 | // Merge user option with defaults 67 | const options = { ...defaults, ...opts }; 68 | if (!options.s3Id) options.s3Id = `${options.s3_path}${uuidv4()}`; 69 | 70 | // Check if debug mode is turned on 71 | if ('debug' in options && options.debug) debugMode = true; 72 | 73 | // Instantiate BusBoy for this request 74 | const busboy = new BusBoy({ 75 | headers: req.headers, 76 | limits: { fileSize: options.max_file_size, files: options.max_files }, 77 | }); 78 | 79 | const logError = (err) => { 80 | const code = uuidv4(); 81 | console.error(`Error Code: ${code}: ${err}`, err); 82 | }; 83 | 84 | // Function to remove file from S3 85 | const removeS3Object = async () => { 86 | try { 87 | const result = await s3.deleteObject({ Key: options.s3Id }).promise(); 88 | console.log( 89 | `S3 Object: "${options.s3Id}" was deleted due to a ClamAV error or virus detection.`, 90 | result 91 | ); 92 | } catch (err) { 93 | logError(err); 94 | } 95 | }; 96 | 97 | // When file has been uploaded to S3 and has been scanned, this function is called 98 | const pipelineComplete = async () => { 99 | if (debugMode) console.log('Pipeline complete!', { s3Details, scanResult, fileInfo }); 100 | 101 | // If file was truncated (because it was too large) 102 | if (fileInfo.truncated) { 103 | // Remove the S3 object 104 | removeS3Object(); 105 | } 106 | 107 | // If the S3 upload threw an error 108 | if (s3Details instanceof Error) { 109 | logError(s3Details); 110 | return reject( 111 | new Error( 112 | 'There was an issue with your upload (Code: 1). Please try again. If you continue to experience issues, please contact Customer Support!' 113 | ) 114 | ); 115 | } 116 | 117 | // If the scan threw an error... 118 | if (scanResult instanceof Error) { 119 | if ('data' in scanResult && scanResult.data.is_infected) { 120 | logError('Stream contained virus(es):', scanResult.data.viruses); 121 | } 122 | 123 | // Not sure what's going on with this ECONNRESET stuff... 124 | if ('code' in scanResult && scanResult.code !== 'ECONNRESET') { 125 | logError(scanResult); 126 | // Remove the S3 object 127 | removeS3Object(); 128 | return reject( 129 | new Error( 130 | 'There was an issue with your upload (Code: 2). Please try again. If you continue to experience issues, please contact Customer Support!' 131 | ) 132 | ); 133 | } 134 | } 135 | 136 | // If the file is infected 137 | else if (scanResult && 'is_infected' in scanResult && scanResult.is_infected === true) { 138 | console.log(`A virus (${scanResult.viruses.join(', ')}) has been uploaded!`); 139 | 140 | // Remove the S3 object 141 | removeS3Object(); 142 | return reject( 143 | new Error( 144 | "The file you've uploaded contained a virus. Please scan your system immediately. If you feel this is in error, please contact Customer Support. Thank you!" 145 | ) 146 | ); 147 | } 148 | 149 | // If we're unsure the file is infected, just note that in the logs 150 | else if (scanResult && 'is_infected' in scanResult && scanResult.is_infected === null) { 151 | console.log( 152 | 'There was an issue scanning the uploaded file... You might need to investigate manually: ', 153 | { s3Details, fileInfo } 154 | ); 155 | } 156 | 157 | // If the file uploaded just fine... 158 | else if (debugMode) console.log('The file uploaded was just fine... Carrying on...'); 159 | 160 | // Resolve upload promise with file info 161 | if (s3Details && 'Location' in s3Details) s3Details.Location = decodeURIComponent(s3Details.Location); // Not sure why this is necessary, but, w/e... 162 | return resolve({ s3Details, fileInfo, fields }); 163 | }; 164 | 165 | // Wait for both the file to be uploaded to S3 and for the scan to complete 166 | // and then call `pipelineComplete` 167 | pipeline.on('part-complete', () => { 168 | // If the full pipeline has completed... 169 | if (scanResult !== null && s3Details !== null) pipelineComplete(); 170 | }); 171 | 172 | // Wait for file(s) 173 | busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { 174 | numFiles += 1; 175 | fileInfo.filesize = 0; 176 | 177 | // Keep track of the size of the file as chunks come in. 178 | // NOTE: This caused files to not fully upload for some reason.... they'd be about 60KB too small... 179 | // file.on('data', (chunk) => { 180 | // fileInfo.filesize += chunk.length; 181 | // }); 182 | 183 | // If the file hits the "max filesize" limit 184 | file.on('limit', () => { 185 | // const pretty_filesize = filesize(fileInfo.filesize); 186 | console.log( 187 | `The file you've provided is over the maximum ${filesize(options.max_file_size)} allowed.`.file 188 | ); 189 | 190 | // Flag file info with something so we can remove from S3 if necessary 191 | fileInfo.truncated = true; 192 | 193 | // Kill upload stream? 194 | file.destroy(); 195 | 196 | // Respond to front-end 197 | return reject( 198 | new Error( 199 | `The file you've provided is over the maximum ${filesize(options.max_file_size)} allowed.` 200 | ) 201 | ); 202 | }); 203 | 204 | // Make sure we're only allowing the specified type of file(s) 205 | if ( 206 | Array.isArray(options.allowed_mimetypes) && 207 | options.allowed_mimetypes.length > 0 && 208 | !options.allowed_mimetypes.includes(mimetype) 209 | ) 210 | return reject(new Error('Invalid file type provided!')); 211 | 212 | // eslint-disable-next-line no-control-regex 213 | const filenameAscii = filename 214 | // eslint-disable-next-line no-control-regex 215 | .replace(/[^\x00-\x7F]/g, '') 216 | .replace(/[,;'"\\/`|><*:$]/g, '') 217 | .replace(/^[.-]+(.*)/, '$1'); 218 | 219 | // Update file info object 220 | fileInfo.filename = filename; 221 | fileInfo.encoding = encoding; 222 | fileInfo.mimetype = mimetype; 223 | fileInfo.filenameAscii = filenameAscii; 224 | fileInfo.s3_filesize = 0; 225 | 226 | // Configure the S3 streaming upload 227 | s3UploadStream = s3Stream.upload({ 228 | Bucket: s3Config.bucket, 229 | Key: options.s3Id, 230 | ContentDisposition: `inline; filename="${filenameAscii}"`, 231 | ContentType: mimetype, 232 | ACL: options.s3_acl, 233 | Metadata: options.s3_metadata, 234 | }); 235 | 236 | // Additional S3 configuration 237 | s3UploadStream.maxPartSize(10 * 1024 ** 2); // 10 MB 238 | s3UploadStream.concurrentParts(5); 239 | s3UploadStream.on('error', (err) => { 240 | s3Details = err; 241 | pipeline.emit('part-complete'); 242 | }); 243 | 244 | // Do this whenever a chunk of the upload has been received by S3 245 | s3UploadStream.on('part', (details) => { 246 | if (file.truncated) s3UploadStream.destroy('S3 uploading has been halted due to an overly-large file.'); 247 | 248 | // Keep track of amount of data uploaded to S3 249 | if (details.receivedSize > fileInfo.s3_filesize) { 250 | fileInfo.filesize = details.receivedSize; 251 | fileInfo.s3_filesize = details.receivedSize; 252 | } 253 | if (debugMode) 254 | console.log( 255 | 'File uploading to S3: ', 256 | `${Math.round((details.uploadedSize / details.receivedSize) * 100)}% (${ 257 | details.uploadedSize 258 | } / ${details.receivedSize})` 259 | ); 260 | }); 261 | 262 | // When the file has been fully uploaded to S3 263 | s3UploadStream.on('uploaded', (details) => { 264 | if (debugMode) console.log('File Uploaded to S3: ', { details, file_size: fileInfo.s3_filesize }); 265 | s3Details = details; 266 | s3Details.filesize = fileInfo.s3_filesize; 267 | pipeline.emit('part-complete'); 268 | }); 269 | 270 | // Get instance of clamscan object 271 | ClamScan.then((clamscan) => { 272 | const av = clamscan.passthrough(); 273 | 274 | // If there's an error scanning the file 275 | av.on('error', (error) => { 276 | scanResult = error; 277 | pipeline.emit('part-complete'); 278 | }) 279 | .on('data', () => { 280 | if (file.truncated) av.destroy('Virus scanning has been halted due to overly-large file.'); 281 | }) 282 | .on('finish', () => { 283 | if (debugMode) console.log('All data has been sent to virus scanner'); 284 | }) 285 | .on('end', () => { 286 | if (debugMode) 287 | console.log('All data has been retrieved by ClamAV and sent on to the destination!'); 288 | }) 289 | .on('scan-complete', (result) => { 290 | if (debugMode) console.log('Scan Complete. Result: ', result); 291 | scanResult = result; 292 | pipeline.emit('part-complete'); 293 | }); 294 | 295 | // Pipe stream through ClamAV and on to S3 296 | file.pipe(av).pipe(s3UploadStream); 297 | }).catch((e) => { 298 | logError(e); 299 | reject(e); 300 | }); 301 | 302 | if (debugMode) console.log('Got a file stream!', filename); 303 | }); 304 | 305 | // When busboy has sent the entire upload to ClamAV 306 | busboy.on('finish', () => { 307 | if (debugMode) console.log('BusBoy has fully flushed to S3 Stream...'); 308 | if (numFiles === 0) pipelineComplete(); 309 | }); 310 | 311 | // Capture the non-file fields too... 312 | busboy.on('field', (fieldname, val) => { 313 | fields[fieldname] = val; 314 | }); 315 | 316 | // Send request to busboy 317 | req.pipe(busboy); 318 | }); 319 | } 320 | 321 | // Generate a unique file ID for this upload 322 | const fileId = uuidv4(); 323 | const s3Id = `some_folder/${fileId}`; 324 | 325 | /** 326 | * This could be some kind of middleware or something. 327 | * 328 | * @param {object} req - An Express Request object 329 | * @param {object} res - An Express Response object 330 | * @param {Function} next - What to do it all goes well 331 | * @returns {void} 332 | */ 333 | async function run(req, res, next) { 334 | // Scan for viruses and upload to S3 335 | try { 336 | const { fileInfo, fields } = await pipe2s3(req, res, { 337 | s3Id, 338 | s3_metadata: { 339 | some_info: 'cool info here', 340 | }, 341 | max_files: 1, 342 | max_file_size: 20 * 1024 ** 2, // 20 MB 343 | allowed_mimetypes: ['application/pdf', 'text/csv', 'text/plain'], 344 | }); 345 | 346 | // Do something now that the files have been scanned and uploaded to S3 (add info to DB or something) 347 | console.log( 348 | 'Cool! Everything worked. Heres the info about the uploaded file as well as the other form fields in the request payload: ', 349 | { fileInfo, fields } 350 | ); 351 | next(); 352 | } catch (err) { 353 | // Ooops... something went wrong. Log it. 354 | console.error(`Error: ${err}`, err); 355 | 356 | try { 357 | // Delete record of file in S3 if anything went wrong 358 | if (s3Id) await s3.deleteObject({ Key: s3Id }).promise(); 359 | } catch (err2) { 360 | const code = uuidv4(); 361 | console.error(`Error Code: ${code}: ${err2}`, err2); 362 | return this.respond_error(res, new Error('We were unable to finish processing the file.'), 400, next); 363 | } 364 | 365 | // Inform the user of the issue... 366 | res.status(400).send('There was an error uploading your file.'); 367 | } 368 | } 369 | 370 | run(); 371 | -------------------------------------------------------------------------------- /examples/stream.js: -------------------------------------------------------------------------------- 1 | const { PassThrough } = require('stream'); 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | const axios = require('axios'); 4 | 5 | const fakeVirusUrl = 'https://www.eicar.org/download/eicar-com-2/?wpdmdl=8842&refresh=661ef9d576b211713306069'; 6 | // const normalFileUrl = 'https://raw.githubusercontent.com/kylefarris/clamscan/sockets/README.md'; 7 | const testUrl = fakeVirusUrl; 8 | 9 | // Initialize the clamscan module 10 | const NodeClam = require('../index'); // Offically: require('clamscan'); 11 | 12 | /** 13 | * Actually run the test. 14 | */ 15 | async function test() { 16 | const clamscan = await new NodeClam().init({ 17 | debugMode: false, 18 | clamdscan: { 19 | bypassTest: true, 20 | host: 'localhost', 21 | port: 3310, 22 | // socket: '/var/run/clamd.scan/clamd.sock', 23 | }, 24 | }); 25 | 26 | // Fetch fake Eicar virus file and pipe it through to our scan screeam 27 | const passthrough = new PassThrough(); 28 | axios.get(testUrl, { responseType: 'stream' }).then((response) => { 29 | response.data.pipe(passthrough); 30 | }); 31 | 32 | try { 33 | const { isInfected, viruses } = await clamscan.scanStream(passthrough); 34 | 35 | // If `isInfected` is TRUE, file is a virus! 36 | if (isInfected === true) { 37 | console.log( 38 | `You've downloaded a virus (${viruses.join( 39 | '' 40 | )})! Don't worry, it's only a test one and is not malicious...` 41 | ); 42 | } else if (isInfected === null) { 43 | console.log("Something didn't work right..."); 44 | } else if (isInfected === false) { 45 | console.log(`The file (${testUrl}) you downloaded was just fine... Carry on...`); 46 | } 47 | process.exit(0); 48 | } catch (err) { 49 | console.error(err); 50 | process.exit(1); 51 | } 52 | } 53 | 54 | test(); 55 | -------------------------------------------------------------------------------- /examples/version.js: -------------------------------------------------------------------------------- 1 | // Initialize the clamscan module 2 | const NodeClam = require('../index'); // Offically: require('clamscan'); 3 | 4 | const ClamScan = new NodeClam().init({ 5 | debugMode: false, 6 | // prettier-ignore 7 | clamdscan: { 8 | // Run scan using command line 9 | path: '/usr/bin/clamdscan', // <-- Secondary fallback to command line -| 10 | configFile: '/etc/clamd.d/daemon.conf', // <---------------------------------------| 11 | // Connect via Host/Port 12 | host: 'localhost', // <-- Primary fallback - | 13 | port: 3310, // <----------------------| 14 | // Connect via socket (preferred) 15 | socket: '/var/run/clamd.scan/clamd.sock', // <-- Preferred connection method 16 | active: true, // Set to 'false' to test getting version info from `clamscan` 17 | }, 18 | // prettier-ignore 19 | clamscan: { 20 | path: '/usr/bin/clamscan', // <-- Worst-case scenario fallback 21 | }, 22 | preference: 'clamdscan', // Set to 'clamscan' to test getting version info from `clamav` 23 | }); 24 | 25 | ClamScan.then(async (av) => { 26 | const result = await av.getVersion(); 27 | console.log('Version: ', result); 28 | }); 29 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jsdoc-route-plugin"], 3 | "recurseDepth": 10, 4 | "source": { 5 | "includePattern": ".+\\.js(doc|x)?$", 6 | "excludePattern": "(^|\\/|\\\\)_" 7 | }, 8 | "sourceType": "module", 9 | "tags": { 10 | "allowUnknownTags": true, 11 | "dictionaries": ["jsdoc", "closure"] 12 | }, 13 | "templates": { 14 | "cleverLinks": false, 15 | "monospaceLinks": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/NodeClamError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clamscan-specific extension of the Javascript Error object 3 | * 4 | * **NOTE**: If string is passed to first param, it will be `msg` and data will be `{}` 5 | * 6 | * @typicalname NodeClamError 7 | */ 8 | class NodeClamError extends Error { 9 | /** 10 | * Creates a new instance of a NodeClamError. 11 | * 12 | * @param {object} data - Additional data we might want to have access to on error 13 | * @param {...any} params - The usual params you'd pass to create an Error object 14 | */ 15 | constructor(data = {}, ...params) { 16 | // eslint-disable-next-line prefer-const 17 | let [msg, fileName, lineNumber] = params; 18 | 19 | if (typeof data === 'string') { 20 | msg = data; 21 | data = {}; 22 | } 23 | 24 | params = [msg, fileName, lineNumber]; 25 | 26 | super(...params); 27 | if (Error.captureStackTrace) Error.captureStackTrace(this, NodeClamError); 28 | 29 | // Custom debugging information 30 | this.data = data; 31 | this.date = new Date(); 32 | } 33 | } 34 | 35 | module.exports = NodeClamError; 36 | -------------------------------------------------------------------------------- /lib/NodeClamTransform.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This code was inspired by the clamav.js package by "yongtang" 3 | * https://github.com/yongtang/clamav.js 4 | */ 5 | const { Transform } = require('stream'); 6 | 7 | /** 8 | * A NodeClam - specific Transform extension that coddles 9 | * chunks into the correct format for a ClamAV socket. 10 | * 11 | * @typicalname NodeClamTransform 12 | */ 13 | class NodeClamTransform extends Transform { 14 | /** 15 | * Creates a new instance of NodeClamTransorm. 16 | * 17 | * @param {object} options - Optional overrides to defaults (same as Node.js Transform) 18 | * @param {boolean} debugMode - If true, do special debug logging 19 | */ 20 | constructor(options, debugMode = false) { 21 | super(options); 22 | this._streaming = false; 23 | this._num_chunks = 0; 24 | this._total_size = 0; 25 | this._debugMode = debugMode; 26 | } 27 | 28 | /** 29 | * Actually does the transorming of the data for ClamAV. 30 | * 31 | * @param {Buffer} chunk - The piece of data to push onto the stream 32 | * @param {string} encoding - The encoding of the chunk 33 | * @param {Function} cb - What to do when done pushing chunk 34 | */ 35 | _transform(chunk, encoding, cb) { 36 | if (!this._streaming) { 37 | this.push('zINSTREAM\0'); 38 | this._streaming = true; 39 | } 40 | 41 | this._total_size += chunk.length; 42 | 43 | const size = Buffer.alloc(4); 44 | size.writeInt32BE(chunk.length, 0); 45 | this.push(size); 46 | this.push(chunk); 47 | this._num_chunks += 1; 48 | // if (this._debugMode) console.log("node-clam: Transforming for ClamAV...", this._num_chunks, chunk.length, this._total_size); 49 | cb(); 50 | } 51 | 52 | /** 53 | * This will flush out the stream when all data has been received. 54 | * 55 | * @param {Function} cb - What to do when done 56 | */ 57 | _flush(cb) { 58 | if (this._debugMode) console.log('node-clam: Received final data from stream.'); 59 | if (!this._readableState.ended) { 60 | const size = Buffer.alloc(4); 61 | size.writeInt32BE(0, 0); 62 | this.push(size); 63 | } 64 | cb(); 65 | } 66 | } 67 | 68 | module.exports = NodeClamTransform; 69 | -------------------------------------------------------------------------------- /lib/getFiles.js: -------------------------------------------------------------------------------- 1 | // Credit to @qwtel on StackOverflow Question: 2 | // https://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search 3 | const { resolve: resolvePath } = require('path'); 4 | const { readdir } = require('fs').promises; 5 | 6 | /** 7 | * Gets a listing of all files (no directories) within a given path. 8 | * By default, it will retrieve files recursively. 9 | * 10 | * @param {string} dir - The directory to get all files of 11 | * @param {boolean} [recursive=true] - If true (default), get all files recursively; False: only get files directly in path 12 | * @returns {Array} - List of all requested path files 13 | */ 14 | const getFiles = async (dir, recursive = true) => { 15 | const items = await readdir(dir, { withFileTypes: true }); 16 | const files = await Promise.all( 17 | items.map((item) => { 18 | const res = resolvePath(dir, item.name); 19 | if (!recursive) { 20 | if (!item.isDirectory()) return res; 21 | return new Promise((resolve) => resolve(null)); 22 | } 23 | return item.isDirectory() ? getFiles(res) : res; 24 | }) 25 | ); 26 | return files.filter(Boolean).flat(); 27 | 28 | // @todo change to this when package required Node 20+ 29 | // const files = await fs.readdir(dir, { recursive: true }); 30 | }; 31 | 32 | module.exports = getFiles; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clamscan", 3 | "version": "2.4.0", 4 | "author": "Kyle Farris (https://infotechinc.com)", 5 | "description": "Use Node JS to scan files on your server with ClamAV's clamscan/clamdscan binary or via TCP to a remote server or local UNIX Domain socket. This is especially useful for scanning uploaded files provided by un-trusted sources.", 6 | "main": "index.js", 7 | "contributors": [ 8 | "dietervds", 9 | "nicolaspeixoto", 10 | "urg ", 11 | "SaltwaterC <Ștefan Rusu>", 12 | "Sjord ", 13 | "chris-maclean ", 14 | "ngraef ", 15 | "caroneater " 16 | ], 17 | "scripts": { 18 | "docs": "jsdoc2md index.js lib/* > API.md", 19 | "format": "node_modules/.bin/prettier '**/*.{js,json}' --write", 20 | "lint": "node_modules/.bin/eslint '**/*.js'", 21 | "lint:fix": "node_modules/.bin/eslint --fix '**/*.js'", 22 | "test": "make test" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git@github.com:kylefarris/clamscan.git" 27 | }, 28 | "keywords": [ 29 | "clamav", 30 | "virus", 31 | "clamscan", 32 | "upload", 33 | "virus scanning", 34 | "clam", 35 | "clamd", 36 | "security" 37 | ], 38 | "license": "MIT", 39 | "engines": { 40 | "node": ">=16.0.0" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/kylefarris/clamscan/issues" 44 | }, 45 | "devDependencies": { 46 | "@babel/eslint-parser": "^7.19.1", 47 | "axios": "^1.2.0", 48 | "chai": "^4.4.1", 49 | "chai-as-promised": "^7.1.1", 50 | "eslint": "^8.28.0", 51 | "eslint-config-airbnb-base": "^15.0.0", 52 | "eslint-config-prettier": "^9.1.0", 53 | "eslint-plugin-chai-friendly": "^0.7.2", 54 | "eslint-plugin-import": "^2.26.0", 55 | "eslint-plugin-jsdoc": "^48.2.1", 56 | "eslint-plugin-prettier": "^5.1.3", 57 | "jsdoc-to-markdown": "^9.0.4", 58 | "mocha": "^10.1.0", 59 | "prettier": "^3.2.5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/bad_files_list.txt: -------------------------------------------------------------------------------- 1 | wont_be_able_to_find_this_file.txt 2 | wont_find_this_one_either.txt -------------------------------------------------------------------------------- /tests/clamd.conf: -------------------------------------------------------------------------------- 1 | # This is a placeholder file for testing. It represents a 2 | # config file in a non-default location. The NodeClam.init() 3 | # function should execute without error if this file is passed 4 | # as clamdscan's `config_file` property and the default config 5 | # file does not exist -------------------------------------------------------------------------------- /tests/eicargen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This simple script simply allows us to generate an eicar file 3 | * as opposed to storing one in our repository which could cause 4 | * it to be immediately removed by antivirus software on contributors' 5 | * machines or, even worse, flagged by github for hosting a virus. 6 | * 7 | * Previously, this library relied on downloading the eircar file from 8 | * the eicar site but that proved slow and unreliable. 9 | */ 10 | const { writeFileSync, copyFileSync, unlinkSync } = require('fs'); 11 | const { Readable } = require('stream'); 12 | 13 | const goodScanDir = `${__dirname}/good_scan_dir`; 14 | const badScanDir = `${__dirname}/bad_scan_dir`; 15 | const mixedScanDir = `${__dirname}/mixed_scan_dir`; 16 | const badScanFile = `${badScanDir}/bad_file_1.txt`; 17 | const spacedVirusFile = `${badScanDir}/bad file 1.txt`; 18 | 19 | // prettier-ignore 20 | const eicarByteArray = [ 21 | 88, 53, 79, 33, 80, 37, 64, 65, 80, 91, 52, 92, 22 | 80, 90, 88, 53, 52, 40, 80, 94, 41, 55, 67, 67, 23 | 41, 55, 125, 36, 69, 73, 67, 65, 82, 45, 83, 84, 24 | 65, 78, 68, 65, 82, 68, 45, 65, 78, 84, 73, 86, 25 | 73, 82, 85, 83, 45, 84, 69, 83, 84, 45, 70, 73, 26 | 76, 69, 33, 36, 72, 43, 72, 42, 27 | ]; 28 | 29 | const eicarBuffer = Buffer.from(eicarByteArray); 30 | 31 | const EicarGen = { 32 | writeFile: () => writeFileSync(badScanFile, eicarBuffer.toString()), 33 | writeMixed: () => { 34 | EicarGen.writeFile(); 35 | copyFileSync(`${goodScanDir}/good_file_1.txt`, `${mixedScanDir}/folder1/good_file_1.txt`); 36 | copyFileSync(`${goodScanDir}/good_file_2.txt`, `${mixedScanDir}/folder2/good_file_2.txt`); 37 | copyFileSync(badScanFile, `${mixedScanDir}/folder1/bad_file_1.txt`); 38 | copyFileSync(badScanFile, `${mixedScanDir}/folder2/bad_file_2.txt`); 39 | }, 40 | emptyMixed: () => { 41 | unlinkSync(`${mixedScanDir}/folder1/good_file_1.txt`); 42 | unlinkSync(`${mixedScanDir}/folder2/good_file_2.txt`); 43 | unlinkSync(`${mixedScanDir}/folder1/bad_file_1.txt`); 44 | unlinkSync(`${mixedScanDir}/folder2/bad_file_2.txt`); 45 | }, 46 | getStream: () => Readable.from(eicarBuffer), 47 | writeFileSpaced: () => writeFileSync(spacedVirusFile, eicarBuffer.toString()), 48 | }; 49 | 50 | module.exports = EicarGen; 51 | -------------------------------------------------------------------------------- /tests/good_files_list.txt: -------------------------------------------------------------------------------- 1 | ./good_scan_dir/good_file_1.txt 2 | ./good_scan_dir/good_file_2.txt 3 | -------------------------------------------------------------------------------- /tests/good_scan_dir/empty_file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylefarris/clamscan/e35a4a8c8df654d7fe9583db5568a4b93b1cf032/tests/good_scan_dir/empty_file.txt -------------------------------------------------------------------------------- /tests/good_scan_dir/good_file_1.txt: -------------------------------------------------------------------------------- 1 | If you delete this, tests will not pass. -------------------------------------------------------------------------------- /tests/good_scan_dir/good_file_2.txt: -------------------------------------------------------------------------------- 1 | A second file for testing -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-useless-catch */ 3 | /* eslint-env mocha */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const { Agent } = require('https'); 7 | const axios = require('axios'); 8 | const chai = require('chai'); 9 | const { promisify } = require('util'); 10 | const { PassThrough, Readable } = require('stream'); 11 | const chaiAsPromised = require('chai-as-promised'); 12 | const eicarGen = require('./eicargen'); 13 | 14 | const should = chai.should(); 15 | const { expect } = chai; 16 | const config = require('./test_config'); 17 | 18 | const goodScanDir = `${__dirname}/good_scan_dir`; 19 | const emptyFile = `${goodScanDir}/empty_file.txt`; 20 | const goodScanFile = `${goodScanDir}/good_file_1.txt`; 21 | const goodScanFile2 = `${goodScanDir}/good_file_2.txt`; 22 | const goodFileList = `${__dirname}/good_files_list.txt`; 23 | const badScanDir = `${__dirname}/bad_scan_dir`; 24 | const badScanFile = `${badScanDir}/bad_file_1.txt`; 25 | const spacedVirusFile = `${badScanDir}/bad file 1.txt`; 26 | const badFileList = `${__dirname}/bad_files_list.txt`; 27 | const mixedScanDir = `${__dirname}/mixed_scan_dir`; 28 | const passthruFile = `${__dirname}/output`; 29 | const noVirusUrl = 'https://raw.githubusercontent.com/kylefarris/clamscan/master/README.md'; 30 | const fakeVirusFalseNegatives = [ 31 | 'eicar: OK.exe', 32 | 'OK.exe', 33 | 'OK eicar.exe', 34 | ': OK.exe', 35 | 'eicar.OK', 36 | ' OK.exe', 37 | 'ok.exe', 38 | 'OK', 39 | ].map((v) => `${badScanDir}/${v}`); 40 | const eicarSignatureRgx = /eicar/i; 41 | 42 | const fsState = promisify(fs.stat); 43 | const fsReadfile = promisify(fs.readFile); 44 | const fsCopyfile = promisify(fs.copyFile); 45 | 46 | chai.use(chaiAsPromised); 47 | 48 | const NodeClam = require('../index'); 49 | 50 | const check = (done, f) => { 51 | try { 52 | f(); 53 | done(); 54 | } catch (e) { 55 | done(e); 56 | } 57 | }; 58 | 59 | // Fix goodFiles list to have full paths 60 | const goodFileListContents = fs.readFileSync(goodFileList).toString(); 61 | const modifiedGoodFileList = `${__dirname}/good_files_list_tmp.txt`; 62 | fs.writeFileSync( 63 | modifiedGoodFileList, 64 | goodFileListContents 65 | .split('\n') 66 | .map((v) => v.replace(/^\./, __dirname)) 67 | .join('\n'), 68 | 'utf8' 69 | ); 70 | 71 | // Help to find unhandled promise rejections 72 | process.on('unhandledRejection', (reason, p) => { 73 | if (reason && typeof reason === 'object' && 'actual' in reason) { 74 | console.log('Reason: ', reason.message, reason.actual); 75 | } 76 | if (reason === null) { 77 | console.log("No reason... here's the promise: ", p); 78 | } 79 | console.log('Unhandled Rejection reason:', reason); 80 | }); 81 | 82 | const resetClam = async (overrides = {}) => { 83 | overrides = overrides || {}; 84 | 85 | const clamdscan = { ...config.clamdscan, ...('clamdscan' in overrides ? overrides.clamdscan : {}) }; 86 | const clamscan = { ...config.clamscan, ...('clamscan' in overrides ? overrides.clamscan : {}) }; 87 | 88 | delete overrides.clamdscan; 89 | delete overrides.clamscan; 90 | 91 | const newConfig = { ...config, ...overrides, clamdscan, clamscan }; 92 | 93 | return new NodeClam().init(newConfig); 94 | }; 95 | 96 | describe('NodeClam Module', () => { 97 | it('should return an object', () => { 98 | NodeClam.should.be.a('function'); 99 | }); 100 | 101 | it('should not be initialized immediately', () => { 102 | const clamscan = new NodeClam(); 103 | should.exist(clamscan.initialized); 104 | expect(clamscan.initialized).to.eql(false); 105 | }); 106 | }); 107 | 108 | describe('Initialized NodeClam module', () => { 109 | it('should have certain config properties defined', async () => { 110 | const clamscan = await resetClam(); 111 | 112 | expect(clamscan.defaults.removeInfected, 'removeInfected').to.not.be.undefined; 113 | expect(clamscan.defaults.quarantineInfected, 'quarantineInfected').to.not.be.undefined; 114 | expect(clamscan.defaults.scanLog, 'scanLog').to.not.be.undefined; 115 | expect(clamscan.defaults.debugMode, 'debugMode').to.not.be.undefined; 116 | expect(clamscan.defaults.fileList, 'fileList').to.not.be.undefined; 117 | expect(clamscan.defaults.scanRecursively, 'scanRecursively').to.not.be.undefined; 118 | expect(clamscan.defaults.clamscan, 'clamscan').to.not.be.undefined; 119 | expect(clamscan.defaults.clamdscan, 'clamdscan').to.not.be.undefined; 120 | expect(clamscan.defaults.preference, 'preference').to.not.be.undefined; 121 | }); 122 | 123 | it('should have the proper global default values set', async () => { 124 | const clamscan = await resetClam(); 125 | expect(clamscan.defaults.removeInfected).to.eql(false); 126 | expect(clamscan.defaults.quarantineInfected).to.eql(false); 127 | expect(clamscan.defaults.scanLog).to.eql(null); 128 | expect(clamscan.defaults.debugMode).to.eql(false); 129 | expect(clamscan.defaults.fileList).to.eql(null); 130 | expect(clamscan.defaults.scanRecursively).to.eql(true); 131 | expect(clamscan.defaults.preference).to.eql('clamdscan'); 132 | }); 133 | 134 | it('should have the proper clamscan default values set', async () => { 135 | const clamscan = await resetClam(); 136 | expect(clamscan.defaults.clamscan.path).to.eql('/usr/bin/clamscan'); 137 | expect(clamscan.defaults.clamscan.db).to.eql(null); 138 | expect(clamscan.defaults.clamscan.scanArchives).to.be.eql(true); 139 | expect(clamscan.defaults.clamscan.active).to.eql(true); 140 | }); 141 | 142 | it('should have the proper clamdscan default values set', async () => { 143 | const clamscan = await resetClam(); 144 | expect(clamscan.defaults.clamdscan.socket).to.eql(false); 145 | expect(clamscan.defaults.clamdscan.host).to.eql(false); 146 | expect(clamscan.defaults.clamdscan.port).to.eql(false); 147 | expect(clamscan.defaults.clamdscan.localFallback).to.eql(true); 148 | expect(clamscan.defaults.clamdscan.path).to.eql('/usr/bin/clamdscan'); 149 | expect(clamscan.defaults.clamdscan.configFile).to.eql(null); 150 | expect(clamscan.defaults.clamdscan.multiscan).to.be.eql(true); 151 | expect(clamscan.defaults.clamdscan.reloadDb).to.eql(false); 152 | expect(clamscan.defaults.clamdscan.active).to.eql(true); 153 | }); 154 | 155 | it('should accept an options array and merge them with the object defaults', async () => { 156 | const clamscan = await resetClam({ 157 | removeInfected: true, 158 | quarantineInfected: config.quarantineInfected, 159 | scanLog: config.scanLog, 160 | debugMode: false, 161 | fileList: `${__dirname}/files_list.txt`, 162 | scanRecursively: true, 163 | clamscan: { 164 | path: config.clamscan.path, 165 | db: '/usr/bin/better_clam_db', 166 | scanArchives: false, 167 | active: false, 168 | }, 169 | clamdscan: { 170 | socket: config.clamdscan.socket, 171 | host: config.clamdscan.host, 172 | port: config.clamdscan.port, 173 | path: config.clamdscan.path, 174 | localFallback: false, 175 | configFile: config.clamdscan.configFile, 176 | multiscan: false, 177 | reloadDb: true, 178 | active: false, 179 | timeout: 300000, 180 | bypassTest: true, 181 | }, 182 | preference: 'clamscan', 183 | }); 184 | 185 | // General 186 | expect(clamscan.settings.removeInfected).to.eql(true); 187 | expect(clamscan.settings.quarantineInfected).to.eql(config.quarantineInfected); 188 | expect(clamscan.settings.scanLog).to.be.eql(config.scanLog); 189 | expect(clamscan.settings.debugMode).to.eql(false); 190 | expect(clamscan.settings.fileList).to.eql(`${__dirname}/files_list.txt`); 191 | expect(clamscan.settings.scanRecursively).to.eql(true); 192 | expect(clamscan.settings.preference).to.eql('clamscan'); 193 | 194 | // clamscan 195 | expect(clamscan.settings.clamscan.path).to.eql(config.clamscan.path); 196 | expect(clamscan.settings.clamscan.db).to.eql('/usr/bin/better_clam_db'); 197 | expect(clamscan.settings.clamscan.scanArchives).to.be.eql(false); 198 | expect(clamscan.settings.clamscan.active).to.eql(false); 199 | 200 | // clamdscan 201 | expect(clamscan.settings.clamdscan.socket).to.eql(config.clamdscan.socket); 202 | expect(clamscan.settings.clamdscan.host).to.eql(config.clamdscan.host); 203 | expect(clamscan.settings.clamdscan.port).to.eql(config.clamdscan.port); 204 | expect(clamscan.settings.clamdscan.path).to.eql(config.clamdscan.path); 205 | expect(clamscan.settings.clamdscan.localFallback).to.eql(false); 206 | expect(clamscan.settings.clamdscan.configFile).to.eql(config.clamdscan.configFile); 207 | expect(clamscan.settings.clamdscan.multiscan).to.be.eql(false); 208 | expect(clamscan.settings.clamdscan.reloadDb).to.eql(true); 209 | expect(clamscan.settings.clamdscan.active).to.eql(false); 210 | expect(clamscan.settings.clamdscan.timeout).to.eql(300000); 211 | expect(clamscan.settings.clamdscan.bypassTest).to.eql(true); 212 | }); 213 | 214 | it('should failover to alternate scanner if preferred scanner is inactive', async () => { 215 | const clamscan = await resetClam({ clamdscan: { active: false } }); 216 | expect(clamscan.scanner).to.eql('clamscan'); 217 | }); 218 | 219 | it('should fail if an invalid scanner preference is supplied when socket or port or host is not specified and localFallback is not false', () => { 220 | expect(resetClam({ preference: 'clamscan' }), 'valid scanner').to.not.be.rejectedWith(Error); 221 | expect(resetClam({ preference: 'badscanner' }), 'invalid scanner').to.not.be.rejectedWith(Error); 222 | expect( 223 | resetClam({ 224 | clamdscan: { localFallback: true, socket: false, port: false, host: false }, 225 | preference: 'badscanner', 226 | }), 227 | 'invalid scanner - no socket or host for local fallback' 228 | ).to.be.rejectedWith(Error); 229 | }); 230 | 231 | it('should fail to load if no active & valid scanner is found and socket is not available', () => { 232 | const clamdScanOptions = { 233 | ...config.clamdscan, 234 | path: `${__dirname}/should/not/exist`, 235 | active: true, 236 | localFallback: true, 237 | socket: false, 238 | port: false, 239 | host: false, 240 | }; 241 | const clamscanOptions = { ...config.clamscan, path: `${__dirname}/should/not/exist`, active: true }; 242 | const options = { ...config, clamdscan: clamdScanOptions, clamscan: clamscanOptions }; 243 | 244 | expect(resetClam(options), 'no active and valid scanner').to.be.rejectedWith(Error); 245 | }); 246 | 247 | it('should fail to load if quarantine path (if specified) does not exist or is not writable and socket is not available', () => { 248 | const clamdScanOptions = { 249 | ...config.clamdscan, 250 | active: true, 251 | localFallback: true, 252 | socket: false, 253 | port: false, 254 | host: false, 255 | }; 256 | const clamscanOptions = { ...config.clamscan, active: true }; 257 | const options = { ...config, clamdscan: clamdScanOptions, clamscan: clamscanOptions, funky: true }; 258 | 259 | options.quarantineInfected = `${__dirname}/should/not/exist`; 260 | expect(resetClam(options), 'bad quarantine path').to.be.rejectedWith(Error); 261 | 262 | options.quarantineInfected = `${__dirname}/infected`; 263 | expect(resetClam(options), 'good quarantine path').to.not.be.rejectedWith(Error); 264 | }); 265 | 266 | it('should set definition database (clamscan) to null if specified db is not found', async () => { 267 | const clamdScanOptions = { ...config.clamdscan, socket: false, port: false, host: false }; 268 | const clamscanOptions = { ...config.clamscan, db: '/usr/bin/better_clam_db', active: true }; 269 | 270 | const options = { ...config, clamdscan: clamdScanOptions, clamscan: clamscanOptions, preference: 'clamscan' }; 271 | 272 | const clamscan = await resetClam(options); 273 | expect(clamscan.settings.clamscan.db).to.be.null; 274 | }); 275 | 276 | it('should set scanLog to null if specified scanLog is not found', async () => { 277 | const options = { 278 | ...config, 279 | scanLog: `${__dirname}/should/not/exist`, 280 | preference: 'clamdscan', 281 | clamdscan: { localFallback: true }, 282 | foobar: true, 283 | }; 284 | 285 | const clamscan = await resetClam(options); 286 | expect(clamscan.settings.scanLog).to.be.null; 287 | }); 288 | 289 | it('should be able have configuration settings changed after instantiation', async () => { 290 | expect(resetClam({ scanLog: null })).to.not.be.rejectedWith(Error); 291 | 292 | const clamscan = await resetClam({ scanLog: null }); 293 | 294 | expect(clamscan.settings.scanLog).to.be.null; 295 | 296 | clamscan.settings.scanLog = config.scanLog; 297 | expect(clamscan.settings.scanLog).to.be.eql(config.scanLog); 298 | }); 299 | 300 | it('should initialize successfully with a custom config file, even if the default config file does not exist', async () => { 301 | /** 302 | * For this test, the test runner needs to ensure that the default clamdscan configuration file 303 | * is *not* available. This file may reside at `../etc/clamav/clamd.conf` 304 | * relative to the clamdscan executable. Making this file unavailable can be as simple as 305 | * renaming it. Only if this file is unavailable will this test be meaningful. If present, 306 | * NodeClam.init will fall back to using the clamscan binary and the default config file. 307 | * 308 | * NodeClam.init should execute successfully using the custom config file only. 309 | */ 310 | const clamscan = await resetClam({ 311 | preference: 'clamdscan', 312 | clamdscan: { 313 | active: true, 314 | configFile: 'tests/clamd.conf', 315 | }, 316 | }); 317 | expect(clamscan.scanner).to.eq('clamdscan'); // Verify that the scanner did not fall back to another binary 318 | }); 319 | 320 | it('should initialize with only a port (no host or socket provided)', async () => { 321 | expect( 322 | resetClam({ 323 | preference: 'clamdscan', 324 | clamdscan: { host: null, port: config.clamdscan.port }, 325 | }) 326 | ).to.not.be.rejectedWith(Error); 327 | }); 328 | }); 329 | 330 | describe('_buildClamFlags', () => { 331 | let clamscan; 332 | beforeEach(async () => { 333 | clamscan = await resetClam(); 334 | }); 335 | 336 | it('should build an array', () => { 337 | expect(clamscan.clamFlags).to.not.be.undefined; 338 | expect(clamscan.clamFlags).to.be.an('array'); 339 | }); 340 | 341 | it('should build a series of flags', () => { 342 | if (clamscan.settings.preference === 'clamdscan') { 343 | const flags = [ 344 | '--no-summary', 345 | '--fdpass', 346 | config.clamdscan.configFile ? `--config-file=${config.clamdscan.configFile}` : null, 347 | '--multiscan', 348 | `--move=${config.quarantineInfected}`, 349 | config.scanLog ? `--log=${config.scanLog}` : null, 350 | ].filter((v) => !!v); 351 | clamscan.clamFlags.should.be.eql(flags); 352 | } else { 353 | clamscan.clamFlags.should.be.eql(['--no-summary', `--log=${config.scanLog}`]); 354 | } 355 | }); 356 | }); 357 | 358 | describe('getVersion', () => { 359 | let clamscan; 360 | beforeEach(async () => { 361 | clamscan = await resetClam(); 362 | }); 363 | 364 | it('should exist', () => { 365 | should.exist(clamscan.getVersion); 366 | }); 367 | it('should be a function', () => { 368 | clamscan.getVersion.should.be.a('function'); 369 | }); 370 | 371 | it('should respond with some version (Promise API)', async () => { 372 | const version = await clamscan.getVersion(); 373 | expect(version).to.be.a('string'); 374 | // This may not always be the case... so, it can be removed if necessary 375 | expect(version).to.match(/^ClamAV \d+\.\d+\.\d+\/\d+\//); 376 | }); 377 | 378 | it('should respond with some version (Callback API)', (done) => { 379 | clamscan.getVersion((err, version) => { 380 | check(done, () => { 381 | expect(err).to.not.be.instanceof(Error); 382 | expect(version).to.be.a('string'); 383 | expect(version).to.match(/^ClamAV \d+\.\d+\.\d+\/\d+\//); 384 | }); 385 | }); 386 | }); 387 | }); 388 | 389 | describe('_initSocket', () => { 390 | let clamscan; 391 | beforeEach(async () => { 392 | clamscan = await resetClam(); 393 | }); 394 | 395 | it('should exist', () => { 396 | should.exist(clamscan._initSocket); 397 | }); 398 | it('should be a function', () => { 399 | clamscan._initSocket.should.be.a('function'); 400 | }); 401 | it('should return a valid socket client', async () => { 402 | const client = await clamscan._initSocket(); 403 | expect(client).to.be.an('object'); 404 | expect(client.writable).to.eql(true); 405 | expect(client.readable).to.eql(true); 406 | expect(client._hadError).to.eql(false); 407 | expect(client).to.respondTo('on'); 408 | expect(client).to.not.respondTo('foobar'); 409 | }); 410 | 411 | // TODO: earlier versions of Node (<=10.0.0) have no public way of determining the timeout 412 | it.skip('should have the same timeout as the one configured through this module', async () => { 413 | clamscan = await resetClam({ clamdscan: { timeout: 300000 } }); 414 | const client = await clamscan._initSocket(); 415 | expect(client.timeout).to.eql(clamscan.settings.clamdscan.timeout); 416 | }); 417 | }); 418 | 419 | describe('ping', () => { 420 | let clamscan; 421 | beforeEach(async () => { 422 | clamscan = await resetClam(); 423 | }); 424 | 425 | it('should exist', () => { 426 | should.exist(clamscan.ping); 427 | }); 428 | it('should be a function', () => { 429 | clamscan.ping.should.be.a('function'); 430 | }); 431 | 432 | it('should respond with a socket client (Promise API)', async () => { 433 | const client = await clamscan.ping(); 434 | expect(client).to.be.an('object'); 435 | expect(client.readyState).to.eql('open'); 436 | expect(client.writable).to.eql(true); 437 | expect(client.readable).to.eql(true); 438 | expect(client._hadError).to.eql(false); 439 | expect(client).to.respondTo('on'); 440 | expect(client).to.respondTo('end'); 441 | expect(client).to.not.respondTo('foobar'); 442 | 443 | client.end(); 444 | }); 445 | 446 | it('should respond with a socket client (Callback API)', (done) => { 447 | clamscan.ping((err, client) => { 448 | check(done, () => { 449 | expect(err).to.not.be.instanceof(Error); 450 | expect(client).to.be.an('object'); 451 | expect(client.writable).to.eql(true); 452 | expect(client.readable).to.eql(true); 453 | expect(client._hadError).to.eql(false); 454 | expect(client).to.respondTo('on'); 455 | expect(client).to.respondTo('end'); 456 | expect(client).to.not.respondTo('foobar'); 457 | }); 458 | }); 459 | }); 460 | }); 461 | 462 | describe('_ping', () => { 463 | let clamscan; 464 | beforeEach(async () => { 465 | clamscan = await resetClam(); 466 | }); 467 | 468 | it('should exist', () => { 469 | should.exist(clamscan._ping); 470 | }); 471 | it('should be a function', () => { 472 | clamscan._ping.should.be.a('function'); 473 | }); 474 | 475 | it('should respond with a socket client (Promise API)', async () => { 476 | const client = await clamscan._ping(); 477 | expect(client).to.be.an('object'); 478 | expect(client.readyState).to.eql('open'); 479 | expect(client.writable).to.eql(true); 480 | expect(client.readable).to.eql(true); 481 | expect(client._hadError).to.eql(false); 482 | expect(client).to.respondTo('on'); 483 | expect(client).to.respondTo('end'); 484 | expect(client).to.not.respondTo('foobar'); 485 | 486 | client.end(); 487 | }); 488 | 489 | it('should respond with a socket client (Callback API)', (done) => { 490 | clamscan._ping((err, client) => { 491 | check(done, () => { 492 | expect(err).to.not.be.instanceof(Error); 493 | expect(client).to.be.an('object'); 494 | expect(client.writable).to.eql(true); 495 | expect(client.readable).to.eql(true); 496 | expect(client._hadError).to.eql(false); 497 | expect(client).to.respondTo('on'); 498 | expect(client).to.respondTo('end'); 499 | expect(client).to.not.respondTo('foobar'); 500 | }); 501 | }); 502 | }); 503 | }); 504 | 505 | describe('isInfected', () => { 506 | let clamscan; 507 | 508 | beforeEach(async () => { 509 | clamscan = await resetClam(); 510 | }); 511 | 512 | it('should exist', () => { 513 | should.exist(clamscan.isInfected); 514 | }); 515 | it('should be a function', () => { 516 | clamscan.isInfected.should.be.a('function'); 517 | }); 518 | 519 | it('should require second parameter to be a callback function (if truthy value provided)', () => { 520 | expect(() => clamscan.isInfected(goodScanFile), 'nothing provided').to.not.throw(Error); 521 | expect(() => clamscan.isInfected(goodScanFile, () => {}), 'good function provided').to.not.throw(Error); 522 | expect(() => clamscan.isInfected(goodScanFile, undefined), 'undefined provided').to.not.throw(Error); 523 | expect(() => clamscan.isInfected(goodScanFile, null), 'null provided').to.not.throw(Error); 524 | expect(() => clamscan.isInfected(goodScanFile, ''), 'empty string provided').to.not.throw(Error); 525 | expect(() => clamscan.isInfected(goodScanFile, false), 'false provided').to.not.throw(Error); 526 | expect(() => clamscan.isInfected(goodScanFile, NaN), 'NaN provided').to.not.throw(Error); 527 | expect(() => clamscan.isInfected(goodScanFile, true), 'true provided').to.throw(Error); 528 | expect(() => clamscan.isInfected(goodScanFile, 5), 'integer provided').to.throw(Error); 529 | expect(() => clamscan.isInfected(goodScanFile, 5.4), 'float provided').to.throw(Error); 530 | expect(() => clamscan.isInfected(goodScanFile, Infinity), 'Infinity provided').to.throw(Error); 531 | expect(() => clamscan.isInfected(goodScanFile, /^\/path/), 'RegEx provided').to.throw(Error); 532 | expect(() => clamscan.isInfected(goodScanFile, ['foo']), 'Array provided').to.throw(Error); 533 | expect(() => clamscan.isInfected(goodScanFile, {}), 'Object provided').to.throw(Error); 534 | }); 535 | 536 | it('should require a string representing the path to a file to be scanned', (done) => { 537 | Promise.all([ 538 | expect(clamscan.isInfected(goodScanFile), 'valid file').to.eventually.eql({ 539 | file: `${__dirname}/good_scan_dir/good_file_1.txt`, 540 | isInfected: false, 541 | viruses: [], 542 | }), 543 | expect(clamscan.isInfected(), 'nothing provided').to.be.rejectedWith(Error), 544 | expect(clamscan.isInfected(undefined), 'undefined provided').to.be.rejectedWith(Error), 545 | expect(clamscan.isInfected(null), 'null provided').to.be.rejectedWith(Error), 546 | expect(clamscan.isInfected(''), 'empty string provided').to.be.rejectedWith(Error), 547 | expect(clamscan.isInfected(false), 'false provided').to.be.rejectedWith(Error), 548 | expect(clamscan.isInfected(true), 'true provided').to.be.rejectedWith(Error), 549 | expect(clamscan.isInfected(5), 'integer provided').to.be.rejectedWith(Error), 550 | expect(clamscan.isInfected(5.4), 'float provided').to.be.rejectedWith(Error), 551 | expect(clamscan.isInfected(Infinity), 'Infinity provided').to.be.rejectedWith(Error), 552 | expect(clamscan.isInfected(/^\/path/), 'RegEx provided').to.be.rejectedWith(Error), 553 | expect(clamscan.isInfected(['foo']), 'Array provided').to.be.rejectedWith(Error), 554 | expect(clamscan.isInfected({}), 'Object provided').to.be.rejectedWith(Error), 555 | expect(clamscan.isInfected(NaN), 'NaN provided').to.be.rejectedWith(Error), 556 | expect( 557 | clamscan.isInfected(() => '/path/to/string'), 558 | 'Function provided' 559 | ).to.be.rejectedWith(Error), 560 | // eslint-disable-next-line no-new-wrappers 561 | expect(clamscan.isInfected(new String('/foo/bar')), 'String object provided').to.be.rejectedWith(Error), 562 | ]).should.notify(done); 563 | }); 564 | 565 | describe('callback-style', () => { 566 | beforeEach(async () => { 567 | clamscan = await resetClam(); 568 | }); 569 | 570 | it('should return error if file not found', (done) => { 571 | clamscan.isInfected(`${__dirname}/missing_file.txt`, (err) => { 572 | check(done, () => { 573 | expect(err).to.be.instanceof(Error); 574 | }); 575 | }); 576 | }); 577 | 578 | it('should supply filename with path back after the file is scanned', (done) => { 579 | clamscan.isInfected(goodScanFile, (err, file) => { 580 | check(done, () => { 581 | expect(err).to.not.be.instanceof(Error); 582 | expect(file).to.not.be.empty; 583 | file.should.be.a('string'); 584 | file.should.eql(goodScanFile); 585 | }); 586 | }); 587 | }); 588 | 589 | it('should respond with FALSE when file is not infected', (done) => { 590 | clamscan.isInfected(goodScanFile, (err, file, isInfected) => { 591 | check(done, () => { 592 | expect(err).to.not.be.instanceof(Error); 593 | expect(isInfected).to.be.a('boolean'); 594 | expect(isInfected).to.eql(false); 595 | }); 596 | }); 597 | }); 598 | 599 | it('should respond with TRUE when non-archive file is infected', (done) => { 600 | eicarGen.writeFile(); 601 | clamscan.isInfected(badScanFile, (err, file, isInfected) => { 602 | check(done, () => { 603 | expect(err).to.not.be.instanceof(Error); 604 | expect(isInfected).to.be.a('boolean'); 605 | expect(isInfected).to.eql(true); 606 | 607 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 608 | }); 609 | }); 610 | }); 611 | 612 | it('should respond with an empty array of viruses when file is NOT infected', (done) => { 613 | clamscan.isInfected(goodScanFile, (err, file, isInfected, viruses) => { 614 | check(done, () => { 615 | expect(err).to.not.be.instanceof(Error); 616 | expect(viruses).to.be.an('array'); 617 | expect(viruses).to.have.length(0); 618 | }); 619 | }); 620 | }); 621 | 622 | it('should respond with name of virus when file is infected', (done) => { 623 | eicarGen.writeFile(); 624 | clamscan.isInfected(badScanFile, (err, file, isInfected, viruses) => { 625 | check(done, () => { 626 | expect(viruses).to.be.an('array'); 627 | expect(viruses).to.have.length(1); 628 | expect(viruses[0]).to.match(eicarSignatureRgx); 629 | 630 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 631 | }); 632 | }); 633 | }); 634 | }); 635 | 636 | describe('promise-style', () => { 637 | beforeEach(async () => { 638 | clamscan = await resetClam(); 639 | }); 640 | 641 | it('should return error if file not found', (done) => { 642 | clamscan.isInfected(`${__dirname}/missing_file.txt`).should.be.rejectedWith(Error).notify(done); 643 | }); 644 | 645 | it('should supply filename with path back after the file is scanned', (done) => { 646 | clamscan 647 | .isInfected(goodScanFile) 648 | .then((result) => { 649 | const { file, isInfected } = result; 650 | expect(file).to.not.be.empty; 651 | file.should.be.a('string'); 652 | file.should.eql(goodScanFile); 653 | done(); 654 | }) 655 | .catch((err) => { 656 | done(err); 657 | }); 658 | }); 659 | 660 | it('should respond with FALSE when file is not infected', (done) => { 661 | clamscan 662 | .isInfected(goodScanFile) 663 | .then((result) => { 664 | const { file, isInfected } = result; 665 | expect(isInfected).to.be.a('boolean'); 666 | expect(isInfected).to.eql(false); 667 | done(); 668 | }) 669 | .catch((err) => { 670 | done(err); 671 | }); 672 | }); 673 | 674 | it('should respond with an empty array of viruses when file is NOT infected', (done) => { 675 | clamscan 676 | .isInfected(goodScanFile) 677 | .then((result) => { 678 | const { viruses } = result; 679 | expect(viruses).to.be.an('array'); 680 | expect(viruses).to.have.length(0); 681 | done(); 682 | }) 683 | .catch((err) => { 684 | done(err); 685 | }); 686 | }); 687 | 688 | it('should respond with name of virus when file is infected', (done) => { 689 | eicarGen.writeFile(); 690 | 691 | clamscan 692 | .isInfected(badScanFile) 693 | .then((result) => { 694 | const { viruses } = result; 695 | expect(viruses).to.be.an('array'); 696 | expect(viruses).to.have.length(1); 697 | expect(viruses[0]).to.match(eicarSignatureRgx); 698 | done(); 699 | }) 700 | .catch((err) => { 701 | done(err); 702 | }) 703 | .finally(() => { 704 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 705 | }); 706 | }); 707 | }); 708 | 709 | describe('async/await-style', () => { 710 | beforeEach(async () => { 711 | clamscan = await resetClam(); 712 | }); 713 | 714 | it('should supply filename with path back after the file is scanned', async () => { 715 | const { file, isInfected } = await clamscan.isInfected(goodScanFile); 716 | expect(file).to.not.be.empty; 717 | file.should.be.a('string'); 718 | file.should.eql(goodScanFile); 719 | }); 720 | 721 | it('should respond with FALSE when file is not infected', async () => { 722 | const { file, isInfected } = await clamscan.isInfected(goodScanFile); 723 | expect(isInfected).to.be.a('boolean'); 724 | expect(isInfected).to.eql(false); 725 | }); 726 | 727 | it('should respond with TRUE when non-archive file is infected', async () => { 728 | eicarGen.writeFile(); 729 | try { 730 | const { isInfected } = await clamscan.isInfected(badScanFile); 731 | expect(isInfected).to.be.a('boolean'); 732 | expect(isInfected).to.eql(true); 733 | // eslint-disable-next-line no-useless-catch 734 | } catch (err) { 735 | throw err; 736 | } finally { 737 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 738 | } 739 | }); 740 | 741 | it('should respond with an empty array of viruses when file is NOT infected', async () => { 742 | const { viruses } = await clamscan.isInfected(goodScanFile); 743 | expect(viruses).to.be.an('array'); 744 | expect(viruses).to.have.length(0); 745 | }); 746 | 747 | it('should respond with name of virus when file is infected', async () => { 748 | eicarGen.writeFile(); 749 | try { 750 | const { viruses } = await clamscan.isInfected(badScanFile); 751 | expect(viruses).to.be.an('array'); 752 | expect(viruses).to.have.length(1); 753 | expect(viruses[0]).to.match(eicarSignatureRgx); 754 | // eslint-disable-next-line no-useless-catch 755 | } catch (err) { 756 | throw err; 757 | } finally { 758 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 759 | } 760 | }); 761 | }); 762 | 763 | describe('Edge Cases', () => { 764 | it('should not provide false negatives in the event of a filename containing "OK"', async () => { 765 | eicarGen.writeFile(); 766 | 767 | try { 768 | // Make copies of the test virus file and rename it to various possible false-negative names 769 | await Promise.all([fakeVirusFalseNegatives.map((v) => fsCopyfile(badScanFile, v))]); 770 | 771 | // Get list of all files to scan 772 | const toScan = [].concat(fakeVirusFalseNegatives).concat([badScanFile]); 773 | 774 | // Scan all the files 775 | // eslint-disable-next-line no-restricted-syntax 776 | for (const virus of toScan) { 777 | // eslint-disable-next-line no-await-in-loop 778 | const { isInfected } = await clamscan.isInfected(virus); 779 | expect(isInfected).to.be.a('boolean'); 780 | expect(isInfected).to.eql(true); 781 | } 782 | 783 | // eslint-disable-next-line no-useless-catch 784 | } catch (err) { 785 | throw err; 786 | } finally { 787 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 788 | fakeVirusFalseNegatives.forEach((v) => { 789 | if (fs.existsSync(v)) fs.unlinkSync(v); 790 | }); 791 | } 792 | }); 793 | 794 | it('should be okay when scanning a file with consecutive (or any) spaces in it while doing a local scan', async () => { 795 | // Make sure we're forced to scan locally 796 | clamscan = await resetClam({ 797 | clamdscan: { host: null, port: null, socket: null, localFallback: true }, 798 | debugMode: false, 799 | quarantineInfected: false, 800 | }); 801 | 802 | // Write virus file with spaces in its name 803 | eicarGen.writeFileSpaced(); 804 | 805 | // Check if infected 806 | try { 807 | const { file, isInfected, viruses } = await clamscan.isInfected(spacedVirusFile); 808 | expect(isInfected, 'isInfected should be true').to.be.true; 809 | expect(file, 'spaced file name should be the same').to.eql(spacedVirusFile); 810 | expect(viruses, 'viruses found should be an array').to.be.an('array'); 811 | expect(viruses, 'viruses found should have 1 element').to.have.length(1); 812 | expect(viruses[0], 'element should match eicar').to.match(eicarSignatureRgx); 813 | // eslint-disable-next-line no-useless-catch 814 | } catch (err) { 815 | throw err; 816 | } finally { 817 | if (fs.existsSync(spacedVirusFile)) fs.unlinkSync(spacedVirusFile); 818 | } 819 | }); 820 | }); 821 | }); 822 | 823 | // This is just an alias to 'isInfected', so, no need to test much more. 824 | describe('scanFile', () => { 825 | let clamscan; 826 | beforeEach(async () => { 827 | clamscan = await resetClam(); 828 | }); 829 | 830 | it('should exist', () => { 831 | should.exist(clamscan.scanFile); 832 | }); 833 | it('should be a function', () => { 834 | clamscan.scanFile.should.be.a('function'); 835 | }); 836 | it('should behave just like isInfected (callback)', (done) => { 837 | clamscan.scanFile(goodScanFile, (err, file, isInfected, viruses) => { 838 | check(done, () => { 839 | expect(err).to.not.be.instanceof(Error); 840 | expect(file).to.not.be.empty; 841 | file.should.be.a('string'); 842 | file.should.eql(goodScanFile); 843 | expect(isInfected).to.be.a('boolean'); 844 | expect(isInfected).to.eql(false); 845 | expect(viruses).to.be.an('array'); 846 | expect(viruses).to.have.length(0); 847 | }); 848 | }); 849 | }); 850 | it('should behave just like isInfected (promise)', (done) => { 851 | clamscan 852 | .scanFile(goodScanFile) 853 | .then((result) => { 854 | const { file, isInfected, viruses } = result; 855 | expect(file).to.not.be.empty; 856 | file.should.be.a('string'); 857 | file.should.eql(goodScanFile); 858 | expect(isInfected).to.be.a('boolean'); 859 | expect(isInfected).to.eql(false); 860 | expect(viruses).to.be.an('array'); 861 | expect(viruses).to.have.length(0); 862 | done(); 863 | }) 864 | .catch((err) => { 865 | done(err); 866 | }); 867 | }); 868 | it('should behave just like isInfected (async/await)', async () => { 869 | const { file, isInfected, viruses } = await clamscan.scanFile(goodScanFile); 870 | expect(file).to.not.be.empty; 871 | expect(file).to.be.a('string'); 872 | expect(file).to.eql(goodScanFile); 873 | expect(isInfected).to.be.a('boolean'); 874 | expect(isInfected).to.eql(false); 875 | expect(viruses).to.be.an('array'); 876 | expect(viruses).to.have.length(0); 877 | }); 878 | }); 879 | 880 | describe('scanFiles', () => { 881 | let clamscan; 882 | beforeEach(async () => { 883 | clamscan = await resetClam({ scanLog: null }); 884 | }); 885 | 886 | it('should exist', () => { 887 | should.exist(clamscan.scanFiles); 888 | }); 889 | 890 | it('should be a function', () => { 891 | clamscan.scanFiles.should.be.a('function'); 892 | }); 893 | 894 | describe('callback api', () => { 895 | it('should return err to the "err" parameter of the "endCb" callback if an array with a bad string is provided as first parameter', (done) => { 896 | clamscan.scanFiles([''], (err, goodFiles, badFiles) => { 897 | check(done, () => { 898 | expect(err).to.be.instanceof(Error); 899 | expect(goodFiles).to.be.empty; 900 | }); 901 | }); 902 | }); 903 | 904 | it('should return err to the "err" parameter of the "endCb" callback if an empty array is provided as first parameter', (done) => { 905 | clamscan.scanFiles([], (err, goodFiles, badFiles) => { 906 | check(done, () => { 907 | expect(err).to.be.instanceof(Error); 908 | expect(goodFiles).to.be.empty; 909 | }); 910 | }); 911 | }); 912 | 913 | it('should return err to the "err" parameter of the "endCb" callback if nothing is provided as first parameter', (done) => { 914 | clamscan.scanFiles(undefined, (err, goodFiles, badFiles) => { 915 | check(done, () => { 916 | expect(err).to.be.instanceof(Error); 917 | expect(goodFiles).to.be.empty; 918 | }); 919 | }); 920 | }); 921 | 922 | it('should return err to the "err" parameter of the "endCb" callback if null is provided as first parameter', (done) => { 923 | clamscan.scanFiles(null, (err, goodFiles, badFiles) => { 924 | check(done, () => { 925 | expect(err).to.be.instanceof(Error); 926 | expect(goodFiles).to.be.empty; 927 | }); 928 | }); 929 | }); 930 | 931 | it('should return err to the "err" parameter of the "endCb" callback if an empty string is provided as first parameter', (done) => { 932 | clamscan.scanFiles('', (err, goodFiles, badFiles) => { 933 | check(done, () => { 934 | expect(err).to.be.instanceof(Error); 935 | expect(goodFiles).to.be.empty; 936 | }); 937 | }); 938 | }); 939 | 940 | it('should return err to the "err" parameter of the "endCb" callback if TRUE is provided as first parameter', (done) => { 941 | clamscan.scanFiles(true, (err, goodFiles, badFiles) => { 942 | check(done, () => { 943 | expect(err).to.be.instanceof(Error); 944 | expect(goodFiles).to.be.empty; 945 | }); 946 | }); 947 | }); 948 | 949 | it('should return err to the "err" parameter of the "endCb" callback if an integer is provided as first parameter', (done) => { 950 | clamscan.scanFiles(5, (err, goodFiles, badFiles) => { 951 | check(done, () => { 952 | expect(err).to.be.instanceof(Error); 953 | expect(goodFiles).to.be.empty; 954 | }); 955 | }); 956 | }); 957 | 958 | it('should return err to the "err" parameter of the "endCb" callback if a float is provided as first parameter', (done) => { 959 | clamscan.scanFiles(5.5, (err, goodFiles, badFiles) => { 960 | check(done, () => { 961 | expect(err).to.be.instanceof(Error); 962 | expect(goodFiles).to.be.empty; 963 | }); 964 | }); 965 | }); 966 | 967 | it('should return err to the "err" parameter of the "endCb" callback if Infinity is provided as first parameter', (done) => { 968 | clamscan.scanFiles(Infinity, (err, goodFiles, badFiles) => { 969 | check(done, () => { 970 | expect(err).to.be.instanceof(Error); 971 | expect(goodFiles).to.be.empty; 972 | }); 973 | }); 974 | }); 975 | 976 | it('should return err to the "err" parameter of the "endCb" callback if a RegEx is provided as first parameter', (done) => { 977 | clamscan.scanFiles(/foobar/, (err, goodFiles, badFiles) => { 978 | check(done, () => { 979 | expect(err).to.be.instanceof(Error); 980 | expect(goodFiles).to.be.empty; 981 | }); 982 | }); 983 | }); 984 | 985 | it('should return err to the "err" parameter of the "endCb" callback if an Standard Object is provided as first parameter', (done) => { 986 | clamscan.scanFiles({}, (err, goodFiles, badFiles) => { 987 | check(done, () => { 988 | expect(err).to.be.instanceof(Error); 989 | expect(goodFiles).to.be.empty; 990 | }); 991 | }); 992 | }); 993 | 994 | it('should return err to the "err" parameter of the "endCb" callback if a NaN is provided as first parameter', (done) => { 995 | clamscan.scanFiles(NaN, (err, goodFiles, badFiles) => { 996 | check(done, () => { 997 | expect(err).to.be.instanceof(Error); 998 | expect(goodFiles).to.be.empty; 999 | }); 1000 | }); 1001 | }); 1002 | 1003 | it('should return err to the "err" parameter of the "endCb" callback if a string-returning function is provided as first parameter', (done) => { 1004 | clamscan.scanFiles( 1005 | () => { 1006 | return goodScanFile; 1007 | }, 1008 | (err, goodFiles, badFiles) => { 1009 | check(done, () => { 1010 | expect(err).to.be.instanceof(Error); 1011 | expect(goodFiles).to.be.empty; 1012 | }); 1013 | } 1014 | ); 1015 | }); 1016 | 1017 | it('should return err to the "err" parameter of the "endCb" callback if a String object is provided as first parameter', (done) => { 1018 | // eslint-disable-next-line no-new-wrappers 1019 | clamscan.scanFiles(new String(goodScanFile), (err, goodFiles, badFiles) => { 1020 | check(done, () => { 1021 | expect(err).to.be.instanceof(Error); 1022 | expect(goodFiles).to.be.empty; 1023 | }); 1024 | }); 1025 | }); 1026 | 1027 | it('should NOT return err to the "err" parameter of the "endCb" callback if an array with a non-empty string or strings is provided as first parameter', (done) => { 1028 | clamscan.scanFiles([goodScanFile], (err, goodFiles, badFiles) => { 1029 | check(done, () => { 1030 | expect(err).to.not.be.instanceof(Error); 1031 | expect(badFiles).to.be.empty; 1032 | expect(goodFiles).to.not.be.empty; 1033 | expect(goodFiles).to.eql([goodScanFile]); 1034 | }); 1035 | }); 1036 | }); 1037 | 1038 | it('should NOT return err to the "err" parameter of the "endCb" callback if a non-empty string is provided as first parameter', (done) => { 1039 | clamscan.scanFiles(goodScanFile, (err, goodFiles, badFiles) => { 1040 | check(done, () => { 1041 | expect(err).to.not.be.instanceof(Error); 1042 | expect(badFiles).to.be.empty; 1043 | expect(goodFiles).to.not.be.empty; 1044 | expect(goodFiles).to.eql([goodScanFile]); 1045 | }); 1046 | }); 1047 | }); 1048 | 1049 | it('should NOT return error to the "err" parameter of the "endCb" callback if nothing is provided as first parameter but fileList is configured in settings', (done) => { 1050 | clamscan.settings.fileList = modifiedGoodFileList; 1051 | clamscan.scanFiles(undefined, (err, goodFiles, badFiles) => { 1052 | check(done, () => { 1053 | expect(err).to.not.be.instanceof(Error); 1054 | expect(goodFiles).to.not.be.empty; 1055 | expect(goodFiles).to.have.length(2); 1056 | expect(badFiles).to.be.empty; 1057 | }); 1058 | }); 1059 | }); 1060 | 1061 | it('should NOT return error to the "err" parameter of the "endCb" callback if nothing is provided as first parameter and fileList is configured in settings but has inaccessible files. Should return list of inaccessible files', (done) => { 1062 | clamscan.settings.fileList = badFileList; 1063 | clamscan.scanFiles(undefined, (err, goodFiles, badFiles, errorFiles) => { 1064 | check(done, () => { 1065 | expect(err).to.not.be.instanceof(Error); 1066 | expect(badFiles).to.be.empty; 1067 | expect(goodFiles).to.be.empty; 1068 | expect(errorFiles).to.be.an('object'); 1069 | 1070 | const fileNames = Object.keys(errorFiles).map((v) => path.basename(v)); 1071 | expect(fileNames).to.be.eql([ 1072 | 'wont_be_able_to_find_this_file.txt', 1073 | 'wont_find_this_one_either.txt', 1074 | ]); 1075 | }); 1076 | }); 1077 | }); 1078 | 1079 | it('should NOT return error to the "err" parameter of the "endCb" callback if FALSE is provided as first parameter but fileList is configured in settings', (done) => { 1080 | clamscan.settings.fileList = modifiedGoodFileList; 1081 | clamscan.scanFiles(false, (err, goodFiles, badFiles) => { 1082 | check(done, () => { 1083 | expect(err).to.not.be.instanceof(Error); 1084 | expect(goodFiles).to.not.be.empty; 1085 | expect(goodFiles).to.have.length(2); 1086 | expect(badFiles).to.be.empty; 1087 | }); 1088 | }); 1089 | }); 1090 | 1091 | it('should NOT return error to the "err" parameter of the "endCb" callback if NaN is provided as first parameter but fileList is configured in settings', (done) => { 1092 | clamscan.settings.fileList = modifiedGoodFileList; 1093 | clamscan.scanFiles(NaN, (err, goodFiles, badFiles) => { 1094 | check(done, () => { 1095 | expect(err).to.not.be.instanceof(Error); 1096 | expect(goodFiles).to.not.be.empty; 1097 | expect(goodFiles).to.have.length(2); 1098 | expect(badFiles).to.be.empty; 1099 | }); 1100 | }); 1101 | }); 1102 | 1103 | it('should NOT return error to the "err" parameter of the "endCb" callback if NULL is provided as first parameter but fileList is configured in settings', (done) => { 1104 | clamscan.settings.fileList = modifiedGoodFileList; 1105 | clamscan.scanFiles(null, (err, goodFiles, badFiles) => { 1106 | check(done, () => { 1107 | expect(err).to.not.be.instanceof(Error); 1108 | expect(goodFiles).to.not.be.empty; 1109 | expect(goodFiles).to.have.length(2); 1110 | expect(badFiles).to.be.empty; 1111 | }); 1112 | }); 1113 | }); 1114 | 1115 | it('should NOT return error to the "err" parameter of the "endCb" callback if an empty string is provided as first parameter but fileList is configured in settings', (done) => { 1116 | clamscan.settings.fileList = modifiedGoodFileList; 1117 | clamscan.scanFiles('', (err, goodFiles, badFiles) => { 1118 | check(done, () => { 1119 | expect(err).to.not.be.instanceof(Error); 1120 | expect(goodFiles).to.not.be.empty; 1121 | expect(goodFiles).to.have.length(2); 1122 | expect(badFiles).to.be.empty; 1123 | }); 1124 | }); 1125 | }); 1126 | 1127 | it('should provide an empty array for the "viruses" parameter if no infected files are found', (done) => { 1128 | clamscan.scanFiles(goodScanFile, (err, goodFiles, badFiles, errorFiles, viruses) => { 1129 | check(done, () => { 1130 | expect(err).to.not.be.instanceof(Error); 1131 | 1132 | expect(goodFiles).to.have.length(1); 1133 | expect(badFiles).to.have.length(0); 1134 | expect(viruses).to.be.an('array'); 1135 | expect(viruses).to.have.length(0); 1136 | }); 1137 | }); 1138 | }); 1139 | 1140 | it('should provide a list of viruses found if the any of the files in the list is infected', (done) => { 1141 | eicarGen.writeFile(); 1142 | 1143 | clamscan.scanFiles( 1144 | [badScanFile, `${__dirname}/good_scan_dir/good_file_1.txt`], 1145 | (err, goodFiles, badFiles, errorFiles, viruses) => { 1146 | // console.log('Check: ', err, goodFiles, badFiles, errorFiles, viruses); 1147 | check(done, () => { 1148 | expect(err).to.not.be.instanceof(Error); 1149 | 1150 | expect(goodFiles).to.not.be.empty; 1151 | expect(goodFiles).to.be.an('array'); 1152 | expect(goodFiles).to.have.length(1); 1153 | 1154 | expect(badFiles).to.not.be.empty; 1155 | expect(badFiles).to.be.an('array'); 1156 | expect(badFiles).to.have.length(1); 1157 | 1158 | expect(errorFiles).to.be.eql({}); 1159 | 1160 | expect(viruses).to.not.be.empty; 1161 | expect(viruses).to.be.an('array'); 1162 | expect(viruses).to.have.length(1); 1163 | expect(viruses[0]).to.match(eicarSignatureRgx); 1164 | 1165 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 1166 | }); 1167 | } 1168 | ); 1169 | }); 1170 | }); 1171 | 1172 | describe('async/await api', () => { 1173 | it('should provide a list of viruses found if the any of the files in the list is infected', async () => { 1174 | eicarGen.writeFile(); 1175 | 1176 | try { 1177 | const { goodFiles, badFiles, errors, viruses } = await clamscan.scanFiles([badScanFile, goodScanFile]); 1178 | expect(goodFiles).to.not.be.empty; 1179 | expect(goodFiles).to.be.an('array'); 1180 | expect(goodFiles).to.have.length(1); 1181 | expect(badFiles).to.not.be.empty; 1182 | expect(badFiles).to.be.an('array'); 1183 | expect(badFiles).to.have.length(1); 1184 | expect(errors).to.be.eql({}); 1185 | expect(viruses).to.not.be.empty; 1186 | expect(viruses).to.be.an('array'); 1187 | expect(viruses).to.have.length(1); 1188 | expect(viruses[0]).to.match(eicarSignatureRgx); 1189 | } catch (err) { 1190 | // This is mostly just here to allow for the `finally` block 1191 | throw err; 1192 | } finally { 1193 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 1194 | } 1195 | }); 1196 | }); 1197 | 1198 | describe('edge cases', () => { 1199 | it('should be fine when one of the filenames has consecutive spaces in it when locally scanning', async () => { 1200 | // Make sure we're forced to scan locally 1201 | clamscan = await resetClam({ 1202 | clamdscan: { host: null, port: null, socket: null, localFallback: true }, 1203 | debugMode: false, 1204 | quarantineInfected: false, 1205 | }); 1206 | 1207 | eicarGen.writeFile(); 1208 | eicarGen.writeFileSpaced(); 1209 | 1210 | try { 1211 | const { goodFiles, badFiles, errors, viruses } = await clamscan.scanFiles([ 1212 | badScanFile, 1213 | goodScanFile, 1214 | spacedVirusFile, 1215 | ]); 1216 | expect(goodFiles).to.not.be.empty; 1217 | expect(goodFiles).to.be.an('array'); 1218 | expect(goodFiles).to.have.length(1); 1219 | expect(badFiles).to.not.be.empty; 1220 | expect(badFiles).to.be.an('array'); 1221 | expect(badFiles).to.have.length(2); 1222 | expect(errors).to.be.eql({}); 1223 | expect(viruses).to.not.be.empty; 1224 | expect(viruses).to.be.an('array'); 1225 | expect(viruses).to.have.length(1); 1226 | expect(viruses[0]).to.match(eicarSignatureRgx); 1227 | } catch (err) { 1228 | // This is mostly just here to allow for the `finally` block 1229 | throw err; 1230 | } finally { 1231 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 1232 | if (fs.existsSync(spacedVirusFile)) fs.unlinkSync(spacedVirusFile); 1233 | } 1234 | }); 1235 | }); 1236 | }); 1237 | 1238 | describe('scanDir', () => { 1239 | let clamscan; 1240 | before(async () => { 1241 | clamscan = await resetClam(); 1242 | }); 1243 | 1244 | it('should exist', () => { 1245 | should.exist(clamscan.scanDir); 1246 | }); 1247 | it('should be a function', () => { 1248 | clamscan.scanDir.should.be.a('function'); 1249 | }); 1250 | 1251 | it('should require a string representing the directory to be scanned', () => { 1252 | expect(clamscan.scanDir(goodScanDir), 'good string provided').to.not.be.rejectedWith(Error); 1253 | expect(clamscan.scanDir(undefined), 'nothing provided').to.be.rejectedWith(Error); 1254 | expect(clamscan.scanDir(null), 'null provided').to.be.rejectedWith(Error); 1255 | expect(clamscan.scanDir(''), 'empty string provided').to.be.rejectedWith(Error); 1256 | expect(clamscan.scanDir(false), 'false provided').to.be.rejectedWith(Error); 1257 | expect(clamscan.scanDir(true), 'true provided').to.be.rejectedWith(Error); 1258 | expect(clamscan.scanDir(5), 'integer provided').to.be.rejectedWith(Error); 1259 | expect(clamscan.scanDir(5.4), 'float provided').to.be.rejectedWith(Error); 1260 | expect(clamscan.scanDir(Infinity), 'Infinity provided').to.be.rejectedWith(Error); 1261 | expect(clamscan.scanDir(/^\/path/), 'RegEx provided').to.be.rejectedWith(Error); 1262 | expect(clamscan.scanDir(['foo']), 'Array provided').to.be.rejectedWith(Error); 1263 | expect(clamscan.scanDir({}), 'Object provided').to.be.rejectedWith(Error); 1264 | expect(clamscan.scanDir(NaN), 'NaN provided').to.be.rejectedWith(Error); 1265 | expect( 1266 | clamscan.scanDir(() => '/path/to/string'), 1267 | 'Function provided' 1268 | ).to.be.rejectedWith(Error); 1269 | // eslint-disable-next-line no-new-wrappers 1270 | expect(clamscan.scanDir(new String('/foo/bar')), 'String object provided').to.be.rejectedWith(Error); 1271 | }); 1272 | 1273 | it('should require the second parameter to be a callback function (if supplied)', () => { 1274 | const cb = (err, goodFiles, badFiles) => {}; 1275 | expect(() => clamscan.scanDir(goodScanDir, cb), 'good function provided').to.not.throw(Error); 1276 | expect(() => clamscan.scanDir(goodScanDir), 'nothing provided').to.not.throw(Error); 1277 | expect(() => clamscan.scanDir(goodScanDir, undefined), 'undefined provided').to.not.throw(Error); 1278 | expect(() => clamscan.scanDir(goodScanDir, null), 'null provided').to.not.throw(Error); 1279 | expect(() => clamscan.scanDir(goodScanDir, ''), 'empty string provided').to.not.throw(Error); 1280 | expect(() => clamscan.scanDir(goodScanDir, false), 'false provided').to.not.throw(Error); 1281 | expect(() => clamscan.scanDir(goodScanDir, NaN), 'NaN provided').to.not.throw(Error); 1282 | expect(() => clamscan.scanDir(goodScanDir, true), 'true provided').to.throw(Error); 1283 | expect(() => clamscan.scanDir(goodScanDir, 5), 'integer provided').to.throw(Error); 1284 | expect(() => clamscan.scanDir(goodScanDir, 5.1), 'float provided').to.throw(Error); 1285 | expect(() => clamscan.scanDir(goodScanDir, Infinity), 'Infinity provided').to.throw(Error); 1286 | expect(() => clamscan.scanDir(goodScanDir, /^\/path/), 'RegEx provided').to.throw(Error); 1287 | expect(() => clamscan.scanDir(goodScanDir, ['foo']), 'Array provided').to.throw(Error); 1288 | expect(() => clamscan.scanDir(goodScanDir, {}), 'Object provided').to.throw(Error); 1289 | }); 1290 | 1291 | it('should return error if directory not found (Promise API)', () => { 1292 | expect(clamscan.scanDir(`${__dirname}/missing_dir`)).to.be.rejectedWith(Error); 1293 | }); 1294 | 1295 | it('should return error if directory not found (Callback API)', (done) => { 1296 | clamscan.scanDir(`${__dirname}/missing_dir`, (err) => { 1297 | check(done, () => { 1298 | expect(err).to.be.instanceof(Error); 1299 | }); 1300 | }); 1301 | }); 1302 | 1303 | it('should supply goodFiles array with scanned path when directory has no infected files (Callback API)', (done) => { 1304 | clamscan.settings.scanRecursively = false; 1305 | clamscan.scanDir(goodScanDir, (err, goodFiles, badFiles) => { 1306 | check(done, () => { 1307 | expect(err).to.not.be.instanceof(Error); 1308 | expect(goodFiles).to.be.an('array'); 1309 | expect(goodFiles).to.have.length(3); 1310 | expect(goodFiles).to.include(goodScanFile); 1311 | expect(goodFiles).to.include(goodScanFile2); 1312 | expect(goodFiles).to.include(emptyFile); 1313 | 1314 | expect(badFiles).to.be.an('array'); 1315 | expect(badFiles).to.be.empty; 1316 | }); 1317 | }); 1318 | }); 1319 | 1320 | it('should supply badFiles array with scanned path when directory has infected files', (done) => { 1321 | // clamscan.settings.scanRecursively = true; 1322 | eicarGen.writeFile(); 1323 | clamscan.scanDir(badScanDir, (err, goodFiles, badFiles) => { 1324 | // if (err) console.error(err); 1325 | check(done, () => { 1326 | expect(err).to.not.be.instanceof(Error); 1327 | expect(badFiles).to.be.an('array'); 1328 | expect(badFiles).to.have.length(1); 1329 | expect(badFiles).to.include(badScanFile); 1330 | 1331 | expect(goodFiles).to.be.an('array'); 1332 | expect(goodFiles).to.be.empty; 1333 | 1334 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 1335 | }); 1336 | }); 1337 | }); 1338 | 1339 | it('should supply an array with viruses found when directory has infected files', (done) => { 1340 | eicarGen.writeFile(); 1341 | 1342 | clamscan.scanDir(badScanDir, (err, _goodFiles, _badFiles, viruses) => { 1343 | check(done, () => { 1344 | expect(err).to.not.be.instanceof(Error); 1345 | expect(viruses).to.not.be.empty; 1346 | expect(viruses).to.be.an('array'); 1347 | expect(viruses).to.have.length(1); 1348 | expect(viruses[0]).to.match(eicarSignatureRgx); 1349 | 1350 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 1351 | }); 1352 | }); 1353 | }); 1354 | 1355 | it('should reply with all the good files, bad files, and viruses from a multi-level directory with some good files and some bad files', (done) => { 1356 | eicarGen.writeMixed(); 1357 | 1358 | clamscan.settings.scanRecursively = true; 1359 | // clamscan.settings.debugMode = true; 1360 | 1361 | clamscan.scanDir(mixedScanDir, (err, goodFiles, badFiles, viruses, numGoodFiles) => { 1362 | check(done, () => { 1363 | const ignoreFiles = ['.DS_Store'].map((v) => `${mixedScanDir}/${v}`); 1364 | goodFiles = goodFiles.filter((v) => !ignoreFiles.includes(v)); 1365 | // console.log('Good Files: ', mixedScanDir, goodFiles); 1366 | // console.log('Bad Files: ', mixedScanDir, badFiles); 1367 | 1368 | expect(err, 'scanDir should not return error').to.not.be.instanceof(Error); 1369 | 1370 | const validBadFiles = [ 1371 | `${mixedScanDir}/folder1/bad_file_1.txt`, 1372 | `${mixedScanDir}/folder2/bad_file_2.txt`, 1373 | ]; 1374 | 1375 | expect(badFiles, 'bad files should be array').to.be.an('array'); 1376 | expect(badFiles, 'bad files should have 2 items').to.have.length(2); 1377 | expect(validBadFiles, 'bad files should include bad_file_1.txt').to.include(badFiles[0].file); 1378 | expect(validBadFiles, 'bad files should include bad_file_2.txt').to.include(badFiles[1].file); 1379 | 1380 | expect(goodFiles, 'good files should be array').to.be.an('array'); 1381 | expect(goodFiles, 'good files should be empty').to.have.length(0); 1382 | expect(numGoodFiles, 'num good files should be 2').to.be.eql(2); 1383 | 1384 | expect(viruses, 'viruses should not be empty').to.not.be.empty; 1385 | expect(viruses, 'viruses should be array').to.be.an('array'); 1386 | expect(viruses, 'viruses should have 1 element').to.have.length(1); 1387 | expect(viruses[0], 'viruses should contain eircar virus signature').to.match(eicarSignatureRgx); 1388 | 1389 | // Just removed the mixed_scan_dir to remove "viruses" 1390 | if (fs.existsSync(mixedScanDir)) eicarGen.emptyMixed(); 1391 | }); 1392 | }); 1393 | }).timeout(15000); 1394 | 1395 | // TODO: Write tests for file_callback 1396 | 1397 | describe('edge cases', () => { 1398 | it('should work when falling back to local scan and there is a file with consecutive spaces in it', async () => { 1399 | // Make sure we're forced to scan locally 1400 | clamscan = await resetClam({ 1401 | clamdscan: { host: null, port: null, socket: null, localFallback: true }, 1402 | debugMode: false, 1403 | quarantineInfected: false, 1404 | }); 1405 | 1406 | eicarGen.writeFile(); 1407 | eicarGen.writeFileSpaced(); 1408 | 1409 | try { 1410 | const { goodFiles, badFiles, viruses } = await clamscan.scanDir(badScanDir); 1411 | 1412 | expect(viruses).to.not.be.empty; 1413 | expect(viruses).to.be.an('array'); 1414 | expect(viruses).to.have.length(1); 1415 | expect(viruses[0]).to.match(eicarSignatureRgx); 1416 | expect(goodFiles).to.be.an('array'); 1417 | expect(goodFiles).to.have.length(0); 1418 | expect(badFiles).to.be.an('array'); 1419 | expect(badFiles).to.have.length(2); 1420 | } catch (err) { 1421 | // This is mostly just here to allow for the `finally` block 1422 | throw err; 1423 | } finally { 1424 | if (fs.existsSync(badScanFile)) fs.unlinkSync(badScanFile); 1425 | if (fs.existsSync(spacedVirusFile)) fs.unlinkSync(spacedVirusFile); 1426 | } 1427 | }); 1428 | }); 1429 | }); 1430 | 1431 | describe('scanStream', () => { 1432 | let clamscan; 1433 | before(async () => { 1434 | clamscan = await resetClam({ scanLog: null, scanRecursively: true }); 1435 | }); 1436 | 1437 | const getGoodStream = () => { 1438 | const rs = new Readable(); 1439 | rs.push('foooooo'); 1440 | rs.push('barrrrr'); 1441 | rs.push(null); 1442 | return rs; 1443 | }; 1444 | 1445 | const getBadStream = () => { 1446 | const passthrough = new PassThrough(); 1447 | eicarGen.getStream().pipe(passthrough); 1448 | return passthrough; 1449 | }; 1450 | 1451 | it('should exist', () => { 1452 | should.exist(clamscan.scanStream); 1453 | }); 1454 | 1455 | it('should be a function', () => { 1456 | clamscan.scanStream.should.be.a('function'); 1457 | }); 1458 | 1459 | it('should throw an error if a stream is not provided to first parameter and no callback is supplied.', (done) => { 1460 | Promise.all([ 1461 | expect(clamscan.scanStream(getGoodStream()), 'good stream provided').to.not.be.rejectedWith(Error), 1462 | expect(clamscan.scanStream(getBadStream()), 'bad stream provided').to.not.be.rejectedWith(Error), 1463 | expect(clamscan.scanStream(), 'nothing provided').to.be.rejectedWith(Error), 1464 | expect(clamscan.scanStream(undefined), 'undefined provided').to.be.rejectedWith(Error), 1465 | expect(clamscan.scanStream(null), 'null provided').to.be.rejectedWith(Error), 1466 | expect(clamscan.scanStream(''), 'empty string provided').to.be.rejectedWith(Error), 1467 | expect(clamscan.scanStream(false), 'false provided').to.be.rejectedWith(Error), 1468 | expect(clamscan.scanStream(NaN), 'NaN provided').to.be.rejectedWith(Error), 1469 | expect(clamscan.scanStream(true), 'true provided').to.be.rejectedWith(Error), 1470 | expect(clamscan.scanStream(42), 'integer provided').to.be.rejectedWith(Error), 1471 | expect(clamscan.scanStream(13.37), 'float provided').to.be.rejectedWith(Error), 1472 | expect(clamscan.scanStream(Infinity), 'Infinity provided').to.be.rejectedWith(Error), 1473 | expect(clamscan.scanStream(/foo/), 'RegEx provided').to.be.rejectedWith(Error), 1474 | expect(clamscan.scanStream([]), 'Array provided').to.be.rejectedWith(Error), 1475 | expect(clamscan.scanStream({}), 'Object provided').to.be.rejectedWith(Error), 1476 | ]).should.notify(done); 1477 | }); 1478 | 1479 | describe('Promise and async/await API', () => { 1480 | it('should throw PromiseRejection with Error when first parameter is not a valid stream.', (done) => { 1481 | clamscan.scanStream(null).should.be.rejectedWith(Error).notify(done); 1482 | }); 1483 | 1484 | it('should not throw PromiseRejection with Error when first parameter IS a valid stream.', (done) => { 1485 | clamscan.scanStream(getGoodStream()).should.not.be.rejectedWith(Error).notify(done); 1486 | }); 1487 | 1488 | it('should throw an error if either socket or host/port combo are invalid.', async () => { 1489 | const clamdScanOptions = { ...config.clamdscan, active: true, socket: false, host: false, port: false }; 1490 | const options = { ...config, clamdscan: clamdScanOptions }; 1491 | 1492 | try { 1493 | clamscan = await resetClam(options); 1494 | clamscan.scanStream(getGoodStream()).should.be.rejectedWith(Error); 1495 | } catch (e) { 1496 | console.error('Annoying error: ', e); 1497 | } 1498 | }); 1499 | 1500 | it('should set the `isInfected` reponse value to FALSE if stream is not infected.', async () => { 1501 | clamscan = await resetClam(); 1502 | const { isInfected, viruses } = await clamscan.scanStream(getGoodStream()); 1503 | expect(isInfected).to.be.a('boolean'); 1504 | expect(isInfected).to.eql(false); 1505 | expect(viruses).to.be.an('array'); 1506 | expect(viruses).to.have.length(0); 1507 | }); 1508 | 1509 | it('should set the `isInfected` reponse value to TRUE if stream IS infected.', async () => { 1510 | const { isInfected, viruses } = await clamscan.scanStream(getBadStream()); 1511 | expect(isInfected).to.be.a('boolean'); 1512 | expect(isInfected).to.eql(true); 1513 | expect(viruses).to.be.an('array'); 1514 | expect(viruses).to.have.length(1); 1515 | }); 1516 | 1517 | it('should not fail when run within a Promise.all()', async () => { 1518 | clamscan = await resetClam(); 1519 | 1520 | const [result1, result2] = await Promise.all([ 1521 | clamscan.scanStream(getGoodStream()), 1522 | clamscan.scanStream(getBadStream()), 1523 | ]); 1524 | 1525 | expect(result1.isInfected).to.be.a('boolean'); 1526 | expect(result1.isInfected).to.eql(false); 1527 | expect(result1.viruses).to.be.an('array'); 1528 | expect(result1.viruses).to.have.length(0); 1529 | 1530 | expect(result2.isInfected).to.be.a('boolean'); 1531 | expect(result2.isInfected).to.eql(true); 1532 | expect(result2.viruses).to.be.an('array'); 1533 | expect(result2.viruses).to.have.length(1); 1534 | }); 1535 | 1536 | it('should not fail when run within a weird Promise.all() (issue #59)', async () => { 1537 | clamscan = await resetClam(); 1538 | 1539 | const items = [getGoodStream(), getBadStream()]; 1540 | 1541 | await Promise.all( 1542 | items.map(async (v, i) => { 1543 | const { isInfected, viruses } = await clamscan.scanStream(v); 1544 | if (i === 0) { 1545 | expect(isInfected).to.be.a('boolean'); 1546 | expect(isInfected).to.eql(false); 1547 | expect(viruses).to.be.an('array'); 1548 | expect(viruses).to.have.length(0); 1549 | } else { 1550 | expect(isInfected).to.be.a('boolean'); 1551 | expect(isInfected).to.eql(true); 1552 | expect(viruses).to.be.an('array'); 1553 | expect(viruses).to.have.length(1); 1554 | } 1555 | }) 1556 | ); 1557 | }); 1558 | }); 1559 | 1560 | describe('Callback API', () => { 1561 | it('should return an error to the first param of the callback, if supplied, when first parameter is not a stream.', (done) => { 1562 | clamscan.scanStream(null, (err, isInfected) => { 1563 | check(done, () => { 1564 | expect(err).to.be.instanceof(Error); 1565 | }); 1566 | }); 1567 | }); 1568 | 1569 | it('should NOT return an error to the first param of the callback, if supplied, when first parameter IS a stream.', (done) => { 1570 | clamscan.scanStream(getGoodStream(), (err, isInfected) => { 1571 | check(done, () => { 1572 | expect(err).to.not.be.instanceof(Error); 1573 | }); 1574 | }); 1575 | }); 1576 | 1577 | it('should throw an error if either socket or host/port combo are invalid.', (done) => { 1578 | clamscan.settings.clamdscan.active = true; 1579 | clamscan.settings.clamdscan.socket = false; 1580 | clamscan.settings.clamdscan.host = false; 1581 | clamscan.settings.clamdscan.port = false; 1582 | 1583 | clamscan.scanStream(null, (err, isInfected) => { 1584 | check(done, () => { 1585 | expect(err).to.be.instanceof(Error); 1586 | }); 1587 | }); 1588 | }); 1589 | 1590 | it('should set the `isInfected` reponse value to FALSE if stream is not infected.', (done) => { 1591 | // Reset from previous test 1592 | clamscan.settings.clamdscan = { ...clamscan.defaults.clamdscan, ...(config.clamdscan || {}) }; 1593 | 1594 | clamscan.scanStream(getGoodStream(), (err, result) => { 1595 | check(done, () => { 1596 | expect(err).to.not.be.instanceof(Error); 1597 | 1598 | const { isInfected, viruses } = result; 1599 | expect(isInfected).to.be.a('boolean'); 1600 | expect(isInfected).to.eql(false); 1601 | expect(viruses).to.be.an('array'); 1602 | expect(viruses).to.have.length(0); 1603 | }); 1604 | }); 1605 | }); 1606 | 1607 | it('should set the `isInfected` reponse value to TRUE if stream IS infected.', (done) => { 1608 | const passthrough = new PassThrough(); 1609 | 1610 | // Fetch fake Eicar virus file and pipe it through to our scan screeam 1611 | eicarGen.getStream().pipe(passthrough); 1612 | 1613 | clamscan.scanStream(passthrough, (err, result) => { 1614 | check(done, () => { 1615 | const { isInfected, viruses } = result; 1616 | expect(isInfected).to.be.a('boolean'); 1617 | expect(isInfected).to.eql(true); 1618 | expect(viruses).to.be.an('array'); 1619 | expect(viruses).to.have.length(1); 1620 | }); 1621 | }); 1622 | }); 1623 | }); 1624 | }); 1625 | 1626 | describe('passthrough', () => { 1627 | let clamscan; 1628 | 1629 | before(async () => { 1630 | clamscan = await resetClam({ scanLog: null }); 1631 | }); 1632 | 1633 | it('should exist', () => { 1634 | should.exist(clamscan.passthrough); 1635 | }); 1636 | 1637 | it('should be a function', () => { 1638 | clamscan.passthrough.should.be.a('function'); 1639 | }); 1640 | 1641 | it('should throw an error if scan host is unreachable', async () => { 1642 | try { 1643 | const clamav = await resetClam({ 1644 | scanLog: null, 1645 | clamdscan: { 1646 | socket: null, 1647 | host: '127.0.0.2', 1648 | port: 65535, 1649 | }, 1650 | }); 1651 | 1652 | const input = fs.createReadStream(goodScanFile); 1653 | const output = fs.createWriteStream(passthruFile); 1654 | const av = clamav.passthrough(); 1655 | 1656 | input.pipe(av).pipe(output); 1657 | if (fs.existsSync(passthruFile)) fs.unlinkSync(passthruFile); 1658 | 1659 | av.on('error', (err) => { 1660 | expect(err).to.be.instanceof(Error); 1661 | }); 1662 | } catch (err) { 1663 | expect(err).to.be.instanceof(Error); 1664 | } 1665 | }); 1666 | 1667 | it('should fire a "scan-complete" event when the stream has been fully scanned and provide a result object that contains "isInfected" and "viruses" properties', (done) => { 1668 | const input = eicarGen.getStream(); 1669 | const output = fs.createWriteStream(passthruFile); 1670 | const av = clamscan.passthrough(); 1671 | 1672 | input.pipe(av).pipe(output); 1673 | if (fs.existsSync(passthruFile)) fs.unlinkSync(passthruFile); 1674 | 1675 | av.on('scan-complete', (result) => { 1676 | check(done, () => { 1677 | expect(result) 1678 | .to.be.an('object') 1679 | .that.has.all.keys('isInfected', 'viruses', 'file', 'resultString', 'timeout'); 1680 | }); 1681 | }); 1682 | }); 1683 | 1684 | it('should indicate that a stream was infected in the "scan-complete" event if the stream DOES contain a virus', (done) => { 1685 | const input = eicarGen.getStream(); 1686 | const output = fs.createWriteStream(passthruFile); 1687 | const av = clamscan.passthrough(); 1688 | 1689 | input.pipe(av).pipe(output); 1690 | if (fs.existsSync(passthruFile)) fs.unlinkSync(passthruFile); 1691 | 1692 | av.on('scan-complete', (result) => { 1693 | check(done, () => { 1694 | const { isInfected, viruses } = result; 1695 | expect(isInfected).to.be.a('boolean'); 1696 | expect(isInfected).to.eql(true); 1697 | expect(viruses).to.be.an('array'); 1698 | expect(viruses).to.have.length(1); 1699 | }); 1700 | }); 1701 | }); 1702 | 1703 | it('should indicate that a stream was NOT infected in the "scan-complete" event if the stream DOES NOT contain a virus', async () => { 1704 | const agent = new Agent({ rejectUnauthorized: false }); 1705 | const input = await axios({ 1706 | method: 'get', 1707 | url: noVirusUrl, 1708 | responseType: 'stream', 1709 | httpsAgent: agent, 1710 | }); 1711 | const output = fs.createWriteStream(passthruFile); 1712 | const av = clamscan.passthrough(); 1713 | 1714 | input.data.pipe(av).pipe(output); 1715 | if (fs.existsSync(passthruFile)) fs.unlinkSync(passthruFile); 1716 | 1717 | av.on('scan-complete', (result) => { 1718 | const { isInfected, viruses } = result; 1719 | expect(isInfected).to.be.a('boolean'); 1720 | expect(isInfected).to.eql(false); 1721 | expect(viruses).to.be.an('array'); 1722 | expect(viruses).to.have.length(0); 1723 | }); 1724 | }); 1725 | 1726 | it('should (for example) have created the file that the stream is being piped to', (done) => { 1727 | const input = fs.createReadStream(goodScanFile); 1728 | const output = fs.createWriteStream(passthruFile); 1729 | const av = clamscan.passthrough(); 1730 | 1731 | input.pipe(av).pipe(output); 1732 | 1733 | output.on('finish', () => { 1734 | Promise.all([ 1735 | expect(fsState(passthruFile), 'get passthru file stats').to.not.be.rejectedWith(Error), 1736 | expect(fsReadfile(passthruFile), 'get passthru file').to.not.be.rejectedWith(Error), 1737 | ]).should.notify(() => { 1738 | if (fs.existsSync(passthruFile)) fs.unlinkSync(passthruFile); 1739 | done(); 1740 | }); 1741 | }); 1742 | }); 1743 | 1744 | it('should have cleanly piped input to output', () => { 1745 | const input = fs.createReadStream(goodScanFile); 1746 | const output = fs.createWriteStream(passthruFile); 1747 | const av = clamscan.passthrough(); 1748 | 1749 | input.pipe(av).pipe(output); 1750 | 1751 | output.on('finish', () => { 1752 | const origFile = fs.readFileSync(goodScanFile); 1753 | const outFile = fs.readFileSync(passthruFile); 1754 | if (fs.existsSync(passthruFile)) fs.unlinkSync(passthruFile); 1755 | 1756 | expect(origFile).to.eql(outFile); 1757 | }); 1758 | }); 1759 | 1760 | // https://github.com/kylefarris/clamscan/issues/82 1761 | it('should not throw multiple callback error', (done) => { 1762 | /** 1763 | * To reliably reproduce the issue in the broken code, it's important that this is an async generator 1764 | * and it emits some chunks larger than the default highWaterMark of 16 KB. 1765 | * 1766 | * @generator 1767 | * @param {number} [i = 10] - Starting index for generating buffers 1768 | * @yields {Buffer} 1KB Buffer fill with NULLs 1769 | */ 1770 | async function* gen(i = 10) { 1771 | while (i < 25) { 1772 | i += 1; 1773 | yield Buffer.from(new Array(i * 1024).fill()); 1774 | } 1775 | } 1776 | 1777 | const input = Readable.from(gen()); 1778 | const av = clamscan.passthrough(); 1779 | 1780 | // The failure case will throw an error and not finish 1781 | input.pipe(av).on('end', done).resume(); 1782 | }); 1783 | 1784 | if (!process.env.CI) { 1785 | it('should handle a 0-byte file', () => { 1786 | const input = fs.createReadStream(emptyFile); 1787 | const output = fs.createWriteStream(passthruFile); 1788 | const av = clamscan.passthrough(); 1789 | 1790 | input.pipe(av).pipe(output); 1791 | 1792 | output.on('finish', () => { 1793 | const origFile = fs.readFileSync(emptyFile); 1794 | const outFile = fs.readFileSync(passthruFile); 1795 | if (fs.existsSync(passthruFile)) fs.unlinkSync(passthruFile); 1796 | 1797 | expect(origFile).to.eql(outFile); 1798 | }); 1799 | }); 1800 | } 1801 | }); 1802 | 1803 | if (process.env.CI) { 1804 | describe('tls', () => { 1805 | let clamscan; 1806 | 1807 | it('Connects to clamd server via a TLS proxy on localhost', async () => { 1808 | clamscan = await resetClam({ 1809 | clamdscan: { 1810 | host: 'localhost', 1811 | port: 3311, 1812 | socket: false, 1813 | tls: true, 1814 | }, 1815 | }); 1816 | (await clamscan.ping()).end(); 1817 | }); 1818 | 1819 | it('Connects to clamd server via a TLS proxy on 127.0.0.1', async () => { 1820 | clamscan = await resetClam({ 1821 | clamdscan: { 1822 | host: '127.0.0.1', 1823 | port: 3311, 1824 | socket: false, 1825 | tls: true, 1826 | }, 1827 | }); 1828 | (await clamscan.ping()).end(); 1829 | }); 1830 | 1831 | it('Connects to clamd server via a TLS proxy on ::1', async () => { 1832 | clamscan = await resetClam({ 1833 | clamdscan: { 1834 | host: '::1', 1835 | port: 3311, 1836 | socket: false, 1837 | tls: true, 1838 | }, 1839 | }); 1840 | (await clamscan.ping()).end(); 1841 | }); 1842 | 1843 | // it('Connects to clamd server via a TLS proxy on implicit localhost', async () => { 1844 | // clamscan = await resetClam({ 1845 | // clamdscan: { 1846 | // host: false, 1847 | // port: 3311, 1848 | // socket: false, 1849 | // tls: true, 1850 | // }, 1851 | // }); 1852 | // }); 1853 | }); 1854 | } 1855 | -------------------------------------------------------------------------------- /tests/stunnel.conf: -------------------------------------------------------------------------------- 1 | [clamd-tls] 2 | accept = :::3311 3 | connect = /var/run/clamd.scan/clamd.sock 4 | cert = /etc/stunnel/cert.pem 5 | key = /etc/stunnel/key.pem 6 | -------------------------------------------------------------------------------- /tests/test_config.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const p = require('node:path'); 3 | 4 | const isMac = process.platform === 'darwin'; 5 | const isGithub = !!process.env.CI; 6 | 7 | // Walk $PATH to find bin 8 | const which = (bin) => { 9 | const path = process.env.PATH.split(p.delimiter); 10 | 11 | let file = ''; 12 | path.find((v) => { 13 | const testPath = v + p.sep + bin; 14 | if (fs.existsSync(testPath)) { 15 | file = testPath; 16 | return true; 17 | } 18 | return false; 19 | }); 20 | 21 | return file; 22 | }; 23 | 24 | const config = { 25 | removeInfected: false, // don't change 26 | quarantineInfected: `${__dirname}/infected`, // required for testing 27 | // scanLog: `${__dirname}/clamscan-log`, // not required 28 | clamscan: { 29 | path: which('clamscan'), // required for testing 30 | }, 31 | clamdscan: { 32 | socket: isMac ? '/opt/homebrew/var/run/clamd.sock' : '/var/run/clamd.scan/clamd.sock', // - can be set to null 33 | host: '127.0.0.1', // required for testing (change for your system) - can be set to null 34 | port: 3310, // required for testing (change for your system) - can be set to null 35 | path: which('clamdscan'), // required for testing 36 | timeout: 1000, 37 | localFallback: false, 38 | // configFile: isMac ? '/opt/homebrew/etc/clamav/clamd.conf' : '/etc/clamd.d/scan.conf', // set if required 39 | }, 40 | // preference: 'clamdscan', // not used if socket/host+port is provided 41 | debugMode: false, 42 | }; 43 | 44 | // Force specific socket when on GitHub Actions 45 | if (isGithub) config.clamdscan.socket = '/var/run/clamav/clamd.ctl'; 46 | 47 | module.exports = config; 48 | --------------------------------------------------------------------------------