├── .gitignore ├── Dockerfile ├── LICENSE ├── assets └── diagram.jpg ├── files └── .keep ├── index.js ├── lib ├── clamp.js ├── fileops.js ├── freshclamp.js ├── s3promises.js ├── server.js ├── snsp.js ├── sqsp.js ├── timestamp.js └── updater.js ├── package.json ├── readme.markdown ├── tainted.txt └── test ├── s3promises.test.js └── sqsp.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | deploy.zip 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6.10 2 | 3 | RUN apt-get update 4 | RUN apt-get update --fix-missing 5 | RUN apt-get install -y build-essential 6 | RUN apt-get install -y libclamav-dev 7 | RUN apt-get install -y clamav-freshclam 8 | RUN freshclam 9 | 10 | RUN apt-get install -y python2.7 11 | 12 | RUN ln -s /usr/bin/nodejs /usr/bin/node 13 | 14 | COPY *.js src/ 15 | COPY *.json src/ 16 | COPY files/ src/files 17 | COPY lib/ src/lib 18 | COPY test/ src/test 19 | 20 | WORKDIR src/ 21 | 22 | RUN npm install 23 | 24 | EXPOSE 8080 25 | 26 | CMD ["node", "index.js"] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Ryan L. Bell and ExecOnline, INC. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /assets/diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/execonline-inc/antivirus/d9101ea869ec51f127358a3c2907ca7c835ec950/assets/diagram.jpg -------------------------------------------------------------------------------- /files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/execonline-inc/antivirus/d9101ea869ec51f127358a3c2907ca7c835ec950/files/.keep -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var clam = require('clam-engine'); 2 | var R = require('ramda'); 3 | var Promise = require('bluebird'); 4 | var SQS = require('./lib/sqsp'); 5 | var S3 = require('./lib/s3promises'); 6 | var SNS = require('./lib/snsp'); 7 | var fops = require('./lib/fileops'); 8 | var clamp = require('./lib/clamp'); 9 | var updater = require('./lib/updater'); 10 | var http = require('./lib/server'); 11 | var timestamp = require('./lib/timestamp'); 12 | 13 | updater.updateVirusDB(); 14 | 15 | // log : Object -> Void 16 | var log = function(a) { 17 | console.log(timestamp() + ": ", a); 18 | }; 19 | 20 | // error : Object -> Void 21 | var error = function(a) { 22 | console.error(timestamp() + ": ", "Error!", a); 23 | }; 24 | 25 | // When there's an error, we log the error data and delete 26 | // the SQS message if possible. If the message can't be deleted, 27 | // then this was an unexpected error and we will just crash and 28 | // restart. 29 | // 30 | // TODO: Consider pushing errors out to SNS topics, to be consumed 31 | // elsewhere. 32 | // 33 | // handleErr : Engine -> Error -> Void 34 | var handleErr = R.curry(function(engine, err) { 35 | error(err); 36 | 37 | Promise.resolve(R.identity(err)) 38 | .then(loop(engine)) 39 | .catch(exit); 40 | }); 41 | 42 | // handleSyntaxErr : Message -> Error -> Void 43 | var handleSyntaxErr = R.curry(function(message, err) { 44 | error(err); 45 | error("SYNTAX ERROR - deleting message: " + JSON.stringify(message)); 46 | 47 | Promise.resolve(R.identity(message)) 48 | .then(SQS.getMessageHandle) 49 | .then(SQS.deleteMessage); 50 | }); 51 | 52 | 53 | var exit = function() { 54 | process.exit(1); 55 | }; 56 | 57 | // Drop into a Promise chain to see what's there. 58 | // spy : a -> a 59 | var spy = function(something) { 60 | log(something); 61 | return something; 62 | }; 63 | 64 | // loop : Engine -> fn 65 | var loop = function(engine) { 66 | return function() { 67 | setImmediate(function() { poll(engine); }); 68 | }; 69 | }; 70 | 71 | var fetchFromS3 = R.compose(S3.fetchObject, S3.details); 72 | 73 | // poll : Engine -> Promise Void 74 | var poll = function(engine) { 75 | return SQS.fetchMessages() 76 | .map(function(message) { 77 | return Promise.resolve(R.identity(message)) 78 | .then(SQS.messageContent) 79 | .then(fetchFromS3) 80 | .then(fops.writeFile) 81 | .then(clamp.scanFile(engine)) 82 | .then(fops.removeFile) 83 | .then(SNS.sendScanResults) 84 | .then(SQS.deleteMessage) 85 | .catch(handleSyntaxErr(message)); 86 | }) 87 | .then(log) 88 | .then(loop(engine)) 89 | .catch(handleErr(engine)); 90 | }; 91 | 92 | clam.createEngine(function(err, engine) { 93 | if (err) return console.log('Err!', err); 94 | 95 | poll(engine); 96 | }); 97 | 98 | // Then every two hours or so, update the virus database again 99 | setInterval(updater.updateVirusDB, 1000 * 60 * 60 * 2); 100 | 101 | // Listen for http to satisfy health checks. 102 | http.server().listen(8080); 103 | -------------------------------------------------------------------------------- /lib/clamp.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var R = require('ramda'); 3 | 4 | // scanFile : Engine 5 | // -> { filename: String } 6 | // -> Promise { *, virus: String | null } 7 | exports.scanFile = R.curry(function(engine, fileData) { 8 | return new Promise(function(resolve, reject) { 9 | engine.scanFile(fileData.filename, function(err, virus) { 10 | if (err) reject(err); 11 | else resolve(R.merge(fileData, { virus: virus })); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /lib/fileops.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var fs = require('fs'); 3 | var R = require('ramda'); 4 | var path = require('path'); 5 | var mkdirp = require('mkdirp'); 6 | 7 | // mktree : String -> Promise String 8 | var mktree = function(dir) { 9 | return new Promise(function(resolve, reject) { 10 | mkdirp(dir, function(err) { 11 | if (err) reject(err); 12 | else resolve(dir); 13 | }); 14 | }); 15 | }; 16 | 17 | // saveFile : { bucket: String, key: String, Body: Buffer} 18 | // -> { *, filename: String } 19 | var saveFile = function(s3Object) { 20 | return new Promise(function(resolve, reject) { 21 | fs.writeFile(s3Object.filename, s3Object.Body, function(err) { 22 | if (err) reject(err); 23 | else resolve(s3Object); 24 | }); 25 | }); 26 | }; 27 | 28 | // writeFile : { bucket: String, key: String, Body: Buffer} 29 | // -> Promise { *, filename: String } 30 | exports.writeFile = function(s3Object) { 31 | var file = "files/" + s3Object.bucket + "/" + s3Object.key; 32 | 33 | return mktree(path.dirname(file)) 34 | .then(function() { 35 | return saveFile(R.merge(s3Object, { filename: file })); 36 | }); 37 | }; 38 | 39 | // removeFile : { filename: String, * } -> Promise { filename: String, * } 40 | exports.removeFile = function(s3Object) { 41 | return new Promise(function(resolve, reject) { 42 | fs.unlink(s3Object.filename, function(err) { 43 | if (err) reject(R.merge(err, s3Object)); 44 | else resolve(s3Object); 45 | }); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /lib/freshclamp.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | var Promise = require('bluebird'); 3 | 4 | // freshclam : Promise Void 5 | var freshclam = function() { 6 | return new Promise(function(resolve, reject) { 7 | var proc = spawn("freshclam", [], { stdio: 'inherit' }); 8 | 9 | proc.on("error", reject); 10 | 11 | proc.on("exit", function(code) { 12 | if (code !== 0) { 13 | reject(new Error("freshclam exited with code " + code)); 14 | } 15 | else { 16 | resolve(); 17 | } 18 | }); 19 | 20 | }); 21 | }; 22 | 23 | exports.exec = freshclam; 24 | -------------------------------------------------------------------------------- /lib/s3promises.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var S3 = new AWS.S3({ 3 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 4 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY 5 | }); 6 | var Promise = require('bluebird'); 7 | var R = require('ramda'); 8 | 9 | // decodeUri :: String -> String 10 | var decodeUri = R.compose(decodeURIComponent, R.replace(/\+/g, " ")); 11 | 12 | // getMessage : { Message: a } -> a | Undefined 13 | var getMessage = R.prop("Message"); 14 | 15 | // parseMessage : { Message: a } -> Object 16 | var parseMessage = R.compose(JSON.parse, getMessage); 17 | 18 | // getRecords : { Records: a } -> [a] 19 | var getRecords = R.prop("Records"); 20 | 21 | // mapRecords : [{ Records: a }] -> [a] 22 | var mapRecords = R.map(getRecords); 23 | 24 | // objectDetail : { s3.bucket.name: String, s3.object.key: String } 25 | // -> { bucket: String, key: String } 26 | var objectDetail = function(obj) { 27 | return { 28 | bucket: obj.s3.bucket.name, 29 | key: decodeUri(obj.s3.object.key) 30 | }; 31 | }; 32 | 33 | // s3ObjectDetails : [{ s3.bucket.name: String, s3.object.key: String }] 34 | // -> [ { bucket: String, key: String }] 35 | var s3ObjectDetails = R.map(objectDetail); 36 | 37 | // processS3Notification : { S3 Notification } 38 | // -> { bucket: String, 39 | // key: String, 40 | // receiptHandle: String 41 | // } 42 | var processS3Notification = R.compose( 43 | R.nth(0), // only expect one s3 file record 44 | s3ObjectDetails, 45 | getRecords, 46 | parseMessage 47 | ); 48 | 49 | // details : { S3 Notification } 50 | // -> { bucket: String, key: String, receiptHandle: String} 51 | exports.details = function details(s3notification) { 52 | return R.merge( 53 | { receiptHandle: s3notification.receiptHandle }, 54 | processS3Notification(s3notification) 55 | ); 56 | }; 57 | 58 | // fetchObject : { S3 Details } -> Promise Object 59 | exports.fetchObject = function fetchObject(details) { 60 | return new Promise(function(resolve, reject) { 61 | S3.getObject({ 62 | Bucket: details.bucket, 63 | Key: details.key 64 | }, function(err, data) { 65 | if (err) reject(R.merge(err, details)); 66 | else resolve(R.merge(data, details)); 67 | }); 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | // server : HTTPServer 4 | var server = function() { 5 | return http.createServer(function(req, res) { 6 | res.writeHead(200, { "Content-Type": 'text/plain' }); 7 | res.write("Still kickin'"); 8 | res.end(); 9 | }); 10 | }; 11 | 12 | exports.server = server; 13 | -------------------------------------------------------------------------------- /lib/snsp.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var SNS = new AWS.SNS({ 3 | region: process.env.AWS_REGION, 4 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 5 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY 6 | }); 7 | var R = require('ramda'); 8 | var Promise = require('bluebird'); 9 | 10 | // Configured using ENV var AWS_AV_CLEAN_TOPIC 11 | // 12 | // cleanAvTopicArn : String 13 | var cleanAvTopicArn = process.env.AWS_AV_CLEAN_TOPIC; 14 | 15 | // Configured using ENV var AWS_AV_INFECTED_TOPIC 16 | // 17 | // infectedAvTopicArn : String 18 | var infectedAvTopicArn = process.env.AWS_AV_INFECTED_TOPIC; 19 | 20 | // topicArn : String | null -> String 21 | var topicArn = function(virus) { 22 | return virus ? infectedAvTopicArn : cleanAvTopicArn; 23 | }; 24 | 25 | // results : Object -> { bucket: String, key: String, virus: String | null } 26 | var results = function(scanObj) { 27 | return { 28 | bucket: scanObj.bucket, 29 | key: scanObj.key, 30 | virus: scanObj.virus 31 | }; 32 | }; 33 | 34 | // virus : { virus: a } -> a 35 | var virus = R.prop("virus"); 36 | 37 | // formattedResults : Object -> String 38 | var formattedResults = R.compose(JSON.stringify, results); 39 | 40 | // sendScanResults : { bucket: String, key: String, virus: String | null } 41 | // -> Promise { bucket: String, key: String, virus: String | null } 42 | var sendScanResults = function(scanObj) { 43 | return new Promise(function(resolve, reject) { 44 | SNS.publish({ 45 | Message: formattedResults(scanObj), 46 | TopicArn: topicArn(virus(scanObj)) 47 | }, function(err) { 48 | if (err) reject(err); 49 | else resolve(scanObj); 50 | }); 51 | }); 52 | }; 53 | 54 | module.exports = { 55 | sendScanResults: sendScanResults 56 | }; 57 | -------------------------------------------------------------------------------- /lib/sqsp.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var Promise = require('bluebird'); 3 | var R = require('ramda'); 4 | 5 | var SQS = new AWS.SQS({ 6 | region: process.env.AWS_REGION, 7 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 8 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY 9 | }); 10 | 11 | // Configured based on the environment (AWS_AV_QUEUE). 12 | // queueUrl : String 13 | var queueUrl = process.env.AWS_AV_QUEUE; 14 | 15 | // getMessages : { Messages: a } -> a | [] 16 | var getMessages = R.propOr([], "Messages"); 17 | 18 | // getMessageBody : { Body: a } -> a 19 | var getMessageBody = R.prop("Body"); 20 | 21 | // parseMessageBody : { Body: a } -> Object 22 | var parseMessageBody = R.compose(JSON.parse, getMessageBody); 23 | 24 | // mapMessageBodies : [{ Body: a }] -> [a] 25 | var mapMessageBodies = R.map(getMessageBody); 26 | 27 | // getMessageHandle : { ReceiptHandle: String, Body: a } 28 | // -> Object 29 | var getMessageHandle = function(data) { 30 | return R.merge( 31 | data, 32 | { receiptHandle: data.ReceiptHandle } 33 | ); 34 | }; 35 | 36 | // messageContent : { ReceiptHandle: String, Body: a } 37 | // -> Object 38 | var messageContent = function(data) { 39 | return R.merge( 40 | parseMessageBody(data), 41 | { receiptHandle: data.ReceiptHandle } 42 | ); 43 | }; 44 | 45 | // fetchMessages : Promise Object 46 | var fetchMessages = function fetchMessages() { 47 | return new Promise(function(resolve, reject) { 48 | SQS.receiveMessage({ 49 | WaitTimeSeconds: 20, 50 | QueueUrl: queueUrl, 51 | MaxNumberOfMessages: 10 52 | }, function(err, data) { 53 | if (err) reject(err); 54 | else resolve(getMessages(data)); 55 | }); 56 | }); 57 | }; 58 | 59 | // deleteMessage : { receiptHandle: String } -> Promise { * } 60 | var deleteMessage = function deleteMessage(obj) { 61 | return new Promise(function(resolve, reject) { 62 | SQS.deleteMessage({ 63 | QueueUrl: queueUrl, 64 | ReceiptHandle: obj.receiptHandle 65 | }, function(err, data) { 66 | if (err) reject(err); 67 | else resolve(obj); 68 | }); 69 | }); 70 | }; 71 | 72 | module.exports = { 73 | messageContent: messageContent, 74 | fetchMessages: fetchMessages, 75 | deleteMessage: deleteMessage, 76 | getMessageHandle: getMessageHandle 77 | }; 78 | -------------------------------------------------------------------------------- /lib/timestamp.js: -------------------------------------------------------------------------------- 1 | // timestamp : String 2 | module.exports = function() { 3 | return new Date().toISOString(); 4 | }; 5 | -------------------------------------------------------------------------------- /lib/updater.js: -------------------------------------------------------------------------------- 1 | var freshie = require('./freshclamp'); 2 | 3 | var completed = function() { 4 | console.log("Update completed!"); 5 | }; 6 | 7 | var updateVirusDB = function() { 8 | console.log("Updating virus database"); 9 | freshie.exec().then(completed); 10 | }; 11 | 12 | exports.updateVirusDB = updateVirusDB; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s3-antivirus", 3 | "version": "1.0.0", 4 | "description": "Anti-virus scanning virtual appliance for s3", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape './test/*.js' | faucet", 8 | "package": "rm -f deploy.zip && npm install && npm prune && zip -r deploy ./* -x './node_modules/*'" 9 | }, 10 | "author": "Ryan L. Bell", 11 | "contributors": "Execonline, Inc.", 12 | "license": "MIT", 13 | "dependencies": { 14 | "aws-sdk": "^2.1.49", 15 | "bluebird": "^2.9.34", 16 | "clam-engine": "^2.0.1", 17 | "mkdirp": "^0.5.1", 18 | "ramda": "^0.17.1" 19 | }, 20 | "devDependencies": { 21 | "faucet": "0.0.1", 22 | "tape": "^4.2.0" 23 | }, 24 | "keywords": ["S3", "AWS", "anti-virus", "antivirus", "clamav"], 25 | "bugs": "https://github.com/execonline-inc/antivirus/issues", 26 | "repository": 27 | { "type": "git" 28 | , "url": "https://github.com/execonline-inc/antivirus.git" 29 | }, 30 | "homepage": "https://github.com/execonline-inc/antivirus" 31 | } 32 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # S3-antivirus (code named: Salve) 2 | 3 | [ClamAV](http://www.clamav.net/index.html) for your S3. 4 | 5 | S3-antivirus runs a small node process that polls SQS for S3 notifications. It 6 | then fetches the S3 object and scans it, reporting the results on SNS topics. 7 | 8 | # getting started (development) 9 | 10 | Install the dependencies. 11 | 12 | ``` 13 | # OS X 14 | $> brew update && brew install clamav 15 | 16 | # Ubuntu 17 | $> apt-get install libclamav-dev clamav-freshclam 18 | 19 | ``` 20 | 21 | Install the virus database 22 | 23 | ``` 24 | $> freshclam 25 | ``` 26 | 27 | Then clone the repo and install the other dependencies. 28 | 29 | ``` 30 | $> git clone https://github.com/execonline-inc/antivirus.git 31 | $> cd antivirus 32 | $> npm install 33 | ``` 34 | 35 | The AWS configuration uses environment variables. The following 36 | variables will need to be set. 37 | 38 | ``` 39 | AWS_ACCESS_KEY_ID # AWS access key 40 | AWS_SECRET_ACCESS_KEY # AWS secret key... shhhh! 41 | AWS_REGION # AWS region (us-east-1, for example) 42 | AWS_AV_QUEUE # SQS queue subscribed to S3 notifications (a url) 43 | AWS_AV_CLEAN_TOPIC # SNS topic arn. For clean file notifications. 44 | AWS_AV_INFECTED_TOPIC # SNS topic arn. For infected file notifications. 45 | ``` 46 | 47 | Then run the script. 48 | 49 | ``` 50 | $> node index.js 51 | ``` 52 | 53 | # the file on it's journey... 54 | 55 | Here's a diagram of how S3-antivirus fits into your infrastructure. 56 | 57 | ![a diagram](https://raw.githubusercontent.com/execonline-inc/antivirus/master/assets/diagram.jpg) 58 | 59 | When S3 adds an object it sends out a notification event. We push these 60 | notifications to an SNS topic. The benefit of SNS is that we can have 61 | many services subscribed to the same topic. 62 | 63 | SNS topics offer no guarantees of delivery. They are fire and forget. If the 64 | virus scanner is down when an event arrives, then that S3 object isn't scanned. 65 | This just won't do. 66 | 67 | Instead of subscribing to the SNS topic, we create an SQS queue. We 68 | subscribe _the queue_ to the S3 events topic. SQS queues offer message 69 | persistence (up to four days, by default). They also have configurable dead 70 | letter fail over. Our virus scanner long polls the SQS queue. This offers us 71 | increased confidence that we will not miss scanning a file. 72 | 73 | When the scanner receives an S3 event from the queue, it downloads the object 74 | from S3 and scans it. 75 | 76 | If a scanned file is clean (no virus detected), we push a notification out 77 | on a "clean" SNS topic. If we detect a virus, we push a notification out on 78 | an "infected" SNS topic. 79 | 80 | The infected topic and the clean topic don't have to be different topics in 81 | practice. We've separated them so that it is easier to configure 82 | the virus scanner to suit your needs. 83 | 84 | Anything that can subscribe to SNS topics can process infected (or clean!) 85 | files. 86 | 87 | # updating virus signatures 88 | 89 | Worried about keeping the virus signature database up-to-date? We've got that 90 | covered, too. 91 | 92 | Clamav uses a tool called `freshclam` to update the signature database. We 93 | ran that earlier to initialize a virus database for development. S3-antivirus 94 | runs `freshclam` whenever the server launches. It then continues to run it 95 | approximately every two hours. Your virus signatures will never be more then 96 | a couple a hours out of date. 97 | 98 | # deployment stories 99 | 100 | S3-antivirus is just node, so deploy it anyway you would deploy node 101 | applications. 102 | 103 | For convenience, we've included a Dockerfile. Docker images provide a 104 | convenient deployment artifact. You can build and test your environment on 105 | your computer. Then deploy _that exact_ environment. This works on any platform 106 | that supports docker images. 107 | 108 | To use the Dockerfile, you first need to 109 | [install Docker](https://docs.docker.com/installation/) 110 | 111 | Build the docker image: 112 | 113 | ``` 114 | $> docker build -t salve . 115 | ``` 116 | 117 | You can run the docker build as a container. 118 | 119 | ``` 120 | $> docker run --env-file some/path/to/salve/environment/file salve 121 | ``` 122 | 123 | Note: Running S3-antivirus this way requires an env file. The env-file format 124 | is this: 125 | 126 | ``` 127 | AWS_ACCESS_KEY_ID= 128 | AWS_SECRET_ACCESS_KEY= 129 | AWS_REGION=us-east-1 130 | AWS_AV_QUEUE=https://some-sqs-queue/url 131 | AWS_AV_CLEAN_TOPIC=some-topic-arn 132 | AWS_AV_INFECTED_TOPIC=another-or-possibly-the-same-topic-arn 133 | ``` 134 | 135 | S3-antivirus was first deployed using Elastic Beanstalk. Elastic Beanstalk is 136 | a low management computing environment offered by AWS. We've included a command 137 | to build a package suitable for deploying in this environment. 138 | 139 | ``` 140 | $> npm run package 141 | # generates `deploy.zip` 142 | ``` 143 | 144 | # deployment checklist 145 | 146 | - Create SNS topics (first time) 147 | - S3 events 148 | - Infected files 149 | - Clean files 150 | - Create SQS queue and subscribe to S3 events topic (first time) 151 | - Upload `deploy.zip` to Elastic Beanstalk 152 | - Configure environment variables (first time only) 153 | 154 | You configure your environment once. Then deploy updates by uploading an new 155 | `deploy.zip`. 156 | 157 | Elastic Beanstalk is already connected to CloudWatch. You can set whatever 158 | alerts are appropriate. ElasticBeanstalk autoscales. Changing the configuration 159 | will support a larger volume of file scans. 160 | 161 | # a word about coding paradigms 162 | 163 | You'll note two key design decisions in this code: 164 | 165 | - The use of a declarative, functional style. We used the excellent Ramda 166 | library. 167 | - All asynchronous code uses Promises. 168 | 169 | The functional style allowed us to compose the necessary behaviors 170 | from primitives provided by Ramda. This left us writing little code 171 | ourselves. 172 | 173 | Promises allowed us to compose and sequence asynchronous 174 | operations without falling into nested callbacks. 175 | -------------------------------------------------------------------------------- /tainted.txt: -------------------------------------------------------------------------------- 1 | X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* 2 | -------------------------------------------------------------------------------- /test/s3promises.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var S3 = require('../lib/s3promises'); 3 | 4 | test('normalizing s3 details', function(t) { 5 | t.plan(1); 6 | 7 | var records = { Records: [ 8 | { 9 | s3: { 10 | bucket: { name: 'foo' }, 11 | object: { key: 'bar' } 12 | } 13 | } 14 | ]}; 15 | 16 | var content = { 17 | Message: JSON.stringify(records), 18 | receiptHandle: 'HANDLE' 19 | }; 20 | 21 | t.same(S3.details(content), { 22 | bucket: 'foo', key: 'bar', receiptHandle: 'HANDLE' 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/sqsp.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var SQS = require('../lib/sqsp'); 3 | 4 | test('content parsing', function(t) { 5 | t.plan(1); 6 | 7 | var content = { 8 | Body: JSON.stringify({ExampleKey: 42}), 9 | ReceiptHandle: 'HANDLE', 10 | }; 11 | 12 | t.same(SQS.messageContent(content), { 13 | ExampleKey: 42, 14 | receiptHandle: 'HANDLE', 15 | }); 16 | }); 17 | --------------------------------------------------------------------------------