Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cloudrun-malware-scanner/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_size = 2

# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,ts}]
charset = utf-8


# Tab indentation (no size specified)
[Makefile]
indent_style = tab
31 changes: 15 additions & 16 deletions cloudrun-malware-scanner/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@
// limitations under the License.

module.exports = {
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "google",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {
}
};
env: {
commonjs: true,
es6: true,
node: true,
},
extends: ['google', 'prettier'],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaVersion: 2020,
},
rules: {},
};
3 changes: 3 additions & 0 deletions cloudrun-malware-scanner/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
57 changes: 57 additions & 0 deletions cloudrun-malware-scanner/amqp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const amqp = require('amqplib');
const { logger } = require('./logger.js');

let connection;
let channel;
let exchangeName;

/**
* Connect to queue
*
* @param {object} config
* @param {string} config.amqpURI
* @param {string} config.exchangeName
* @param {string} config.exchangeType
* @return {Promise<void>}
*/
async function connectQueue(config) {
try {
exchangeName = config.exchangeName;

logger.info('Connecting to RabbitMQ server', { amqpURI: config.amqpURI });
connection = await amqp.connect(config.amqpURI);
logger.info('Connected to RabbitMQ server');

channel = await connection.createChannel();

// https://amqp-node.github.io/amqplib/channel_api.html#channel_assertExchange
await channel.assertExchange(config.exchangeName, config.exchangeType, {
durable: false,
});
} catch (error) {
logger.error('Error connecting RabbitMQ', error);
}
}

/**
* Send message to the queue
* @param {string} object
* @param {string} routingKey
* @return {Promise<void>}
*/
const sendMessageToQueue = async ({ message, routingKey }) => {
try {
logger.info('Publishing message', { exchangeName, routingKey });
const buffer = Buffer.from(JSON.stringify(message));
await channel.publish(exchangeName, routingKey, buffer);
} catch (error) {
logger.error(error);
}
};

module.exports = {
connectQueue,
connection,
channel,
sendMessageToQueue,
};
53 changes: 32 additions & 21 deletions cloudrun-malware-scanner/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@
* limitations under the License.
*/

const {Storage} = require('@google-cloud/storage');
const {logger} = require('./logger.js');
const { Storage } = require('@google-cloud/storage');
const { logger } = require('./logger.js');
const pkgJson = require('./package.json');


/**
* Configuration object.
*
Expand All @@ -37,8 +36,9 @@ const pkgJson = require('./package.json');
*/
const Config = null;

const storage = new Storage({userAgent: `cloud-solutions/${
pkgJson.name}-usage-v${pkgJson.version}`});
const storage = new Storage({
userAgent: `cloud-solutions/${pkgJson.name}-usage-v${pkgJson.version}`,
});

/**
* Read configuration from JSON configuration file, verify
Expand All @@ -51,15 +51,14 @@ const storage = new Storage({userAgent: `cloud-solutions/${
async function readAndVerifyConfig(configFile) {
logger.info(`Using configuration file: ${configFile}`);


/** @type {Config} */
let config;

try {
config = require(configFile);
delete config.comments;
} catch (e) {
logger.fatal({err: e}, `Unable to read JSON file from ${configFile}`);
logger.fatal({ err: e }, `Unable to read JSON file from ${configFile}`);
throw new Error(`Invalid configuration ${configFile}`);
}

Expand All @@ -75,21 +74,32 @@ async function readAndVerifyConfig(configFile) {
for (let x = 0; x < config.buckets.length; x++) {
const buckets = config.buckets[x];
for (const bucketType of ['unscanned', 'clean', 'quarantined']) {
if (!(await checkBucketExists(
buckets[bucketType], `config.buckets[${x}].${bucketType}`))) {
if (
!(await checkBucketExists(
buckets[bucketType],
`config.buckets[${x}].${bucketType}`,
))
) {
success = false;
}
}
if (buckets.unscanned === buckets.clean ||
buckets.unscanned === buckets.quarantined ||
buckets.clean === buckets.quarantined) {
if (
buckets.unscanned === buckets.clean ||
buckets.unscanned === buckets.quarantined ||
buckets.clean === buckets.quarantined
) {
logger.fatal(
`Error in ${configFile} buckets[${x}]: bucket names are not unique`);
`Error in ${configFile} buckets[${x}]: bucket names are not unique`,
);
success = false;
}
}
if (!(await checkBucketExists(
config.ClamCvdMirrorBucket, 'ClamCvdMirrorBucket'))) {
if (
!(await checkBucketExists(
config.ClamCvdMirrorBucket,
'ClamCvdMirrorBucket',
))
) {
success = false;
}

Expand All @@ -99,7 +109,6 @@ async function readAndVerifyConfig(configFile) {
return config;
}


/**
* Check that given bucket exists. Returns true on success
*
Expand All @@ -117,13 +126,15 @@ async function checkBucketExists(bucketName, configName) {
// This is used in place of Bucket.exists() to avoid the need for
// Project/viewer permission.
try {
await storage.bucket(bucketName)
.getFiles({maxResults: 1, prefix: 'zzz', autoPaginate: false});
await storage
.bucket(bucketName)
.getFiles({ maxResults: 1, prefix: 'zzz', autoPaginate: false });
return true;
} catch (e) {
logger.fatal(`Error in config: cannot view files in "${configName}" : ${
bucketName} : ${e.message}`);
logger.debug({err: e});
logger.fatal(
`Error in config: cannot view files in "${configName}" : ${bucketName} : ${e.message}`,
);
logger.debug({ err: e });
return false;
}
}
Expand Down
23 changes: 23 additions & 0 deletions cloudrun-malware-scanner/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
version: '3'

services:
clamav:
image: mkodockx/docker-clamav:alpine
container_name: clamav
ports:
- 3310:3310
volumes:
- clam:/var/lib/clamav

rabbitmq:
image: rabbitmq:3-management-alpine
container_name: 'rabbitmq'
ports:
- 5672:5672
- 15672:15672
environment:
- RABBITMQ_DEFAULT_USER=guest
- RABBITMQ_DEFAULT_PASS=guest

volumes:
clam:
42 changes: 26 additions & 16 deletions cloudrun-malware-scanner/gcs-proxy-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@
* limitations under the License.
*/

const {GoogleAuth} = require('google-auth-library');
const {logger} = require('./logger.js');
const { GoogleAuth } = require('google-auth-library');
const { logger } = require('./logger.js');
// eslint-disable-next-line no-unused-vars
const {Config, readAndVerifyConfig} = require('./config.js');
const { Config, readAndVerifyConfig } = require('./config.js');
const httpProxy = require('http-proxy');

const TOKEN_REFRESH_THRESHOLD_MILLIS = 60000;

const googleAuth = new GoogleAuth();


// access token for GCS requests - will be refreshed shortly before it expires
let accessToken;
let accessTokenRefreshTimeout;
Expand All @@ -43,19 +42,28 @@ async function accessTokenRefresh() {
}

const client = await googleAuth.getClient();
if (!client.credentials?.expiry_date ||
client.credentials.expiry_date <=
new Date().getTime() + TOKEN_REFRESH_THRESHOLD_MILLIS) {
if (
!client.credentials?.expiry_date ||
client.credentials.expiry_date <=
new Date().getTime() + TOKEN_REFRESH_THRESHOLD_MILLIS
) {
accessToken = await googleAuth.getAccessToken();
logger.info(`Refreshed Access token; expires at ${
new Date(client.credentials.expiry_date).toISOString()}`);
logger.info(
`Refreshed Access token; expires at ${new Date(
client.credentials.expiry_date,
).toISOString()}`,
);
}
const nextCheckDate =
new Date(client.credentials.expiry_date - TOKEN_REFRESH_THRESHOLD_MILLIS);
const nextCheckDate = new Date(
client.credentials.expiry_date - TOKEN_REFRESH_THRESHOLD_MILLIS,
);
logger.debug(
`Next access token refresh check at ${nextCheckDate.toISOString()}`);
`Next access token refresh check at ${nextCheckDate.toISOString()}`,
);
accessTokenRefreshTimeout = setTimeout(
accessTokenRefresh, nextCheckDate.getTime() - new Date().getTime());
accessTokenRefresh,
nextCheckDate.getTime() - new Date().getTime(),
);
}

/**
Expand All @@ -67,7 +75,8 @@ async function accessTokenRefresh() {
*/
function handleProxyError(err, req, res) {
logger.error(
`Failed to proxy to GCS for path ${req.url}, returning code 500: ${err}`);
`Failed to proxy to GCS for path ${req.url}, returning code 500: ${err}`,
);
res.writeHead(500, {
'Content-Type': 'text/plain',
});
Expand Down Expand Up @@ -113,8 +122,9 @@ async function setupGcsReverseProxy() {
const PROXY_PORT = process.env.PROXY_PORT || 8888;

proxy.listen(PROXY_PORT, 'localhost');
logger.info(`GCS authenticating reverse proxy listenting on port ${
PROXY_PORT} for requests to ${clamCvdMirrorBucket}`);
logger.info(
`GCS authenticating reverse proxy listenting on port ${PROXY_PORT} for requests to ${clamCvdMirrorBucket}`,
);
}

/**
Expand Down
6 changes: 2 additions & 4 deletions cloudrun-malware-scanner/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

const bunyan = require('bunyan');
const {LoggingBunyan} = require('@google-cloud/logging-bunyan');
const { LoggingBunyan } = require('@google-cloud/logging-bunyan');
const pkgJson = require('./package.json');

// Create logging client.
Expand All @@ -30,7 +30,5 @@ const loggingBunyan = new LoggingBunyan({

exports.logger = bunyan.createLogger({
name: pkgJson.name,
streams: [
loggingBunyan.stream('info'),
],
streams: [loggingBunyan.stream('info')],
});
Loading