diff --git a/.env b/.env
index af27a4c..dbbc136 100644
--- a/.env
+++ b/.env
@@ -1,3 +1,4 @@
CWD=`dirname $0:A`
-alias mocli="${CWD}/mocli.sh"
\ No newline at end of file
+alias mocli="${CWD}/mocli.sh"
+mocli gen-cmp
diff --git a/Dockerfile b/Dockerfile
index 220f138..95cb345 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,21 +1,28 @@
FROM node:11-alpine
-LABEL version="1.0"
+LABEL version="1.2"
LABEL description="Linux alpine with node:11 and chromium browser"
RUN set -x \
&& apk update \
&& apk upgrade \
&& echo @edge http://nl.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories \
- && echo @edge http://nl.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories \
- && apk add --no-cache \
+ && echo @edge http://nl.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories
+
+RUN apk add --no-cache \
+ bash \
+ python3
+
+RUN apk add --no-cache \
chromium@edge \
harfbuzz@edge \
nss@edge \
freetype@edge \
- ttf-freefont@edge \
- && rm -rf /var/cache/* \
- && mkdir /var/cache/apk
+ ttf-freefont@edge
+
+RUN pip3 install awscli
+
+RUN rm -rf /var/cache/* && mkdir /var/cache/apk
WORKDIR /opt/mo
diff --git a/README.md b/README.md
index 5d66486..096a9a9 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# MetaDefender browser extension
+[![metadefender-browser-extension](https://david-dm.org/opswat/metadefender-browser-extension.svg)](https://david-dm.org/opswat/metadefender-browser-extension)
+
## Intro
OPSWAT File Security for Chrome, provides the ability to quickly scan a file for malware prior to download directly from the browser. At the moment this only works with google chrome, but can be extended to any other browser.
@@ -167,6 +169,8 @@ To rebuild the docker image run:
```bash
IMG=opswat/metadefender-browser-extension && VER=1.0
docker build -f Dockerfile . -t ${IMG}:${VER} -t ${IMG}:latest
+docker push ${IMG}:${VER}
+docker push ${IMG}:latest
```
## Contributing
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index ae10cfc..299bc37 100755
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -122,6 +122,13 @@
"message": "Save clean files
Download clean files on my computer after they are scanned.
",
"description": ""
},
+ "safeUrl": {
+ "message": "Enable safe URLBrowse the web in safety and alert me when accessing harmful websites.
",
+ "description": ""
+ },
+ "safeUrlSub": {
+ "message": "Read more about Safe URL Redirect"
+ },
"searcHistory": {
"message": "Search history"
},
diff --git a/app/config/common.json b/app/config/common.json
index 0a86985..58d0e0f 100755
--- a/app/config/common.json
+++ b/app/config/common.json
@@ -56,5 +56,7 @@
"g.files.metadefender.com"
],
- "browserNotificationTimeout": 5000
+ "browserNotificationTimeout": 5000,
+
+ "maxCleanUrls": 1000
}
\ No newline at end of file
diff --git a/app/html/extension/settings.html b/app/html/extension/settings.html
index e7240ac..8fe5635 100755
--- a/app/html/extension/settings.html
+++ b/app/html/extension/settings.html
@@ -15,6 +15,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/scripts/background/background-task.js b/app/scripts/background/background-task.js
index 2f137b2..d6d1155 100644
--- a/app/scripts/background/background-task.js
+++ b/app/scripts/background/background-task.js
@@ -15,6 +15,7 @@ import FileProcessor from '../common/file-processor';
import cookieManager from './cookie-manager';
import DownloadsManager from './download-manager';
import { goToTab } from './navigation';
+import SafeUrl from './safe-url';
const MCL_CONFIG = MCL.config;
@@ -54,6 +55,7 @@ class BackgroundTask {
chrome.notifications.onClosed.addListener(() => { });
browserMessage.addListener(this.messageListener.bind(this));
+
}
async init() {
@@ -82,6 +84,8 @@ class BackgroundTask {
this.setApikey(cookie.value);
}
});
+
+ SafeUrl.toggle(settings.safeUrl);
}
/**
@@ -222,6 +226,9 @@ class BackgroundTask {
if (settings.saveCleanFiles !== saveCleanFiles) {
this.updateContextMenu();
}
+
+ SafeUrl.toggle(settings.safeUrl);
+
break;
}
case BROWSER_EVENT.SCAN_FILES_UPDATED: {
diff --git a/app/scripts/background/safe-url.js b/app/scripts/background/safe-url.js
new file mode 100644
index 0000000..ec7024c
--- /dev/null
+++ b/app/scripts/background/safe-url.js
@@ -0,0 +1,131 @@
+import '../common/config';
+
+import xhr from 'xhr';
+
+/**
+ * Metadefender Cloud Endpoint for url check
+ */
+const safeRedirectEndpoint = `${MCL.config.mclDomain}/safe-redirect/`;
+
+/**
+ * A list of urls that are currently redirecting.
+ */
+const activeRedirects = new Set();
+
+/**
+ * A list of urls that are infected.
+ */
+const infectedUrls = new Set();
+
+/**
+ * A list of urls that are not infected.
+ * Used to speed-up navigation after fist check.
+ */
+const cleanUrls = new Set();
+
+/**
+ * Removes old urls that were marked as clean
+ */
+const removeOldUrls = () => {
+ if (cleanUrls.size > MCL.config.maxCleanUrls) {
+ const firstValue = cleanUrls.values().next().value;
+ cleanUrls.delete(firstValue);
+ }
+};
+
+/**
+ * Save the url as infected or not.
+ * @param {string} testUrl the url to be tested
+ * @param {string} urlValidator test endpoint
+ */
+const handleUrlValidatorResponse = (testUrl, err, res) => {
+ if (err) {
+ cleanUrls.add(testUrl);
+ return;
+ }
+
+ try {
+ if (res.headers.status === '400') {
+ infectedUrls.add(testUrl);
+ }
+ else {
+ cleanUrls.add(testUrl);
+ }
+ }
+ catch (e) {
+ cleanUrls.add(testUrl);
+ }
+ removeOldUrls();
+};
+
+const isSafeUrl = (testUrl, urlValidator) => {
+ xhr.get(urlValidator, {sync: true, headers: {noredirect: true}}, (err, res) => handleUrlValidatorResponse(testUrl, err, res));
+};
+
+/**
+ * Intercept web request before the request is made
+ * and redirect valid urls to safe-redirect endpoint.
+ *
+ * @param {string} safeRedirectEndpoint
+ * @param {*} details chrome.webRequest event details
+ * @returns a BlockingResponse https://developer.chrome.com/extensions/webRequest
+ */
+const doSafeRedirect = (details) => {
+ const tabUrl = details.url || '';
+
+ if (!tabUrl.startsWith(safeRedirectEndpoint)) {
+ const shortUrl = tabUrl.split('?')[0];
+ if (!activeRedirects.has(shortUrl) && details.initiator !== 'null') {
+ if (!cleanUrls.has(shortUrl)) {
+ const safeUrl = safeRedirectEndpoint + encodeURIComponent(tabUrl);
+ isSafeUrl(shortUrl, safeUrl);
+ if (infectedUrls.has(shortUrl)) {
+ activeRedirects.add(shortUrl);
+ return {
+ redirectUrl: safeUrl
+ };
+ }
+ }
+ }
+ activeRedirects.delete(shortUrl);
+ if (infectedUrls.has(shortUrl)) {
+ infectedUrls.delete(shortUrl);
+ }
+ }
+};
+
+/**
+ * Verifies urls using metadefender cloud safe-redirect feature.
+ */
+class SafeUrl {
+ constructor() {
+ this.enabled = false;
+ this.toggle = this.toggle.bind(this);
+
+ this._infectedUrls = infectedUrls;
+ this._cleanUrls = cleanUrls;
+ this._doSafeRedirect = doSafeRedirect;
+ this._handleUrlValidatorResponse = handleUrlValidatorResponse;
+ }
+
+ toggle(enable) {
+ if (this.enabled === enable) {
+ return;
+ }
+
+ this.enabled = enable;
+ if (this.enabled) {
+ chrome.webRequest.onBeforeRequest.addListener(doSafeRedirect, {
+ urls: ['http://*/*', 'https://*/*'],
+ types: ['main_frame']
+ }, ['blocking']);
+ }
+ else {
+ chrome.webRequest.onBeforeRequest.removeListener(doSafeRedirect);
+ }
+
+ return this.enabled;
+ }
+}
+
+export default new SafeUrl();
\ No newline at end of file
diff --git a/app/scripts/background/safe-url.spec.js b/app/scripts/background/safe-url.spec.js
new file mode 100644
index 0000000..4fe342e
--- /dev/null
+++ b/app/scripts/background/safe-url.spec.js
@@ -0,0 +1,122 @@
+import chrome from 'sinon-chrome';
+import sinon from 'sinon';
+import xhr from 'xhr';
+import SafeUrl from './safe-url';
+
+describe('app/scripts/background/safe-url.js', () => {
+
+ beforeAll(() => {
+ global.chrome = chrome;
+ });
+
+ describe('enabled', () => {
+
+ it('should be disabled by default', () => {
+ expect(SafeUrl.enabled).toBeFalsy();
+ });
+ });
+
+ describe('toggle', () => {
+
+ it('should enable safe url', () => {
+ chrome.webRequest.onBeforeRequest.addListener.resetHistory();
+
+ SafeUrl.enabled = false;
+ const enabled = SafeUrl.toggle(true);
+ const callArgs = chrome.webRequest.onBeforeRequest.addListener.getCall(0).args;
+
+ expect(enabled).toBeTruthy();
+ expect(callArgs.length).toEqual(3);
+ expect(typeof callArgs[0]).toBe('function');
+ expect(callArgs[1]).toEqual({urls: ['http://*/*', 'https://*/*'], types: ['main_frame']});
+ expect(callArgs[2]).toEqual(['blocking']);
+ });
+
+ it('shold not change enabled state', () => {
+ SafeUrl.enabled = false;
+ expect(typeof SafeUrl.toggle(false)).toBe('undefined');
+ });
+
+ it('should disable safe url', () => {
+ chrome.webRequest.onBeforeRequest.removeListener.resetHistory();
+
+ SafeUrl.enabled = true;
+ const enabled = SafeUrl.toggle(false);
+ const callArgs = chrome.webRequest.onBeforeRequest.removeListener.getCall(0).args;
+
+ expect(enabled).toBeFalsy();
+ expect(callArgs.length).toEqual(1);
+ expect(typeof callArgs[0]).toBe('function');
+ });
+
+ });
+
+ describe('_handleUrlValidatorResponse', () => {
+
+ it('should mark url as infected', () => {
+ SafeUrl._handleUrlValidatorResponse('a1', 'error', {});
+ expect(SafeUrl._cleanUrls.has('a1')).toBeTruthy();
+
+ SafeUrl._handleUrlValidatorResponse('a2', '', {headers: {status: '200'}});
+ expect(SafeUrl._cleanUrls.has('a2')).toBeTruthy();
+
+ SafeUrl._handleUrlValidatorResponse('a3', '', {headers: {status: '404'}});
+ expect(SafeUrl._cleanUrls.has('a3')).toBeTruthy();
+ });
+
+ it('should mark the url as infected', () => {
+ SafeUrl._handleUrlValidatorResponse('b1', '', {headers: {status: '400'}});
+ expect(SafeUrl._infectedUrls.has('b1')).toBeTruthy();
+ });
+
+ });
+
+ describe('_doSafeRedirect', () => {
+
+ it('should not redirect metadefender safe redirect endpoint', () => {
+ expect(typeof SafeUrl._doSafeRedirect({type: 'main_frame', url: `${MCL.config.mclDomain}/safe-redirect/`})).toBe('undefined');
+ });
+
+ it('should not redirect clean URLs', () => {
+ const testUrl = 'http://metadefender.opswat.com';
+ const details = {
+ url: testUrl
+ };
+ sinon.stub(xhr, 'get').callsFake(() => SafeUrl._handleUrlValidatorResponse(testUrl, 'error', {}));
+
+ const redirect = SafeUrl._doSafeRedirect(details);
+ expect(typeof redirect).toBe('undefined');
+
+ xhr.get.restore();
+ });
+
+ it('should redirect infected URLs', () => {
+ const testUrl = 'http://infected.url';
+ const details = {
+ url: testUrl
+ };
+ sinon.stub(xhr, 'get').callsFake(() => SafeUrl._handleUrlValidatorResponse(testUrl, '', {headers: {status: '400'}}));
+
+ const redirect = SafeUrl._doSafeRedirect(details);
+ expect(typeof redirect.redirectUrl).toBe('string');
+ expect(redirect.redirectUrl.startsWith(`${MCL.config.mclDomain}/safe-redirect/`)).toBeTruthy();
+ expect(redirect.redirectUrl.endsWith(encodeURIComponent(testUrl))).toBeTruthy();
+
+ xhr.get.restore();
+ });
+
+ it('should not redirect infected URLs on second access', () => {
+ const testUrl = 'http://infected.url/access';
+ const details = {
+ url: testUrl
+ };
+ sinon.stub(xhr, 'get').callsFake(() => SafeUrl._handleUrlValidatorResponse(testUrl, '', {headers: {status: '400'}}));
+
+ SafeUrl._doSafeRedirect(details);
+ expect(typeof SafeUrl._doSafeRedirect(details)).toBe('undefined');
+
+ xhr.get.restore();
+ });
+ });
+
+});
\ No newline at end of file
diff --git a/app/scripts/common/metascan-client.js b/app/scripts/common/metascan-client.js
index 6a1e972..9798341 100755
--- a/app/scripts/common/metascan-client.js
+++ b/app/scripts/common/metascan-client.js
@@ -1,7 +1,7 @@
'use strict';
import 'chromereload/devonly';
-import { SANITIZATION } from '../constants/workflows';
+import { SANITIZATION, UNARCHIVE } from '../constants/workflows';
import browserMessage from '../common/browser/browser-message';
import { BROWSER_EVENT } from '../common/browser/browser-message-event';
@@ -169,6 +169,7 @@ function fileUpload({fileName, fileData, sampleSharing, password, canBeSanitized
'Content-Type': 'application/octet-stream',
'samplesharing': sampleSharing,
'filename': fileName,
+ 'rule': UNARCHIVE,
'x-source': 'chrome_extension'
};
@@ -177,7 +178,7 @@ function fileUpload({fileName, fileData, sampleSharing, password, canBeSanitized
}
if (canBeSanitized) {
- additionalHeaders.rule = SANITIZATION;
+ additionalHeaders.rule += ',' + SANITIZATION;
}
let options = {
diff --git a/app/scripts/common/persistent/settings.js b/app/scripts/common/persistent/settings.js
index 81dd4b6..8be04b7 100644
--- a/app/scripts/common/persistent/settings.js
+++ b/app/scripts/common/persistent/settings.js
@@ -19,6 +19,7 @@ function Settings() {
shareResults: true,
showNotifications: true,
saveCleanFiles: false,
+ safeUrl: false,
// methods
init: init,
@@ -61,7 +62,8 @@ async function save(){
scanDownloads: this.scanDownloads,
shareResults: this.shareResults,
showNotifications: this.showNotifications,
- saveCleanFiles: this.saveCleanFiles
+ saveCleanFiles: this.saveCleanFiles,
+ safeUrl: this.safeUrl
}});
await BrowserMessage.send({event: BROWSER_EVENT.SETTINGS_UPDATED});
}
diff --git a/app/scripts/constants/workflows.js b/app/scripts/constants/workflows.js
index 1bca674..0fe213f 100644
--- a/app/scripts/constants/workflows.js
+++ b/app/scripts/constants/workflows.js
@@ -1 +1,2 @@
export const SANITIZATION = 'sanitize';
+export const UNARCHIVE = 'unarchive';
diff --git a/karma.conf.js b/karma.conf.js
index 2fab9a5..069c594 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -6,6 +6,11 @@ module.exports = function(config) {
frameworks: [
'jasmine'
],
+ client: {
+ jasmine: {
+ random: false
+ }
+ },
files: [
'test/karma.js'
],
diff --git a/mocli.sh b/mocli.sh
index ac37a40..ac511ac 100755
--- a/mocli.sh
+++ b/mocli.sh
@@ -6,35 +6,58 @@ cd ${CWD}
ENVS=('local' 'dev' 'qa' 'prod')
ENV="qa"
+declare -A CMDS_DESC
+CMDS_DESC=(
+ [developer]="Setup your developer environment"
+ [config]="Overwrite common configuration file with specified environment. Usage: mocli config "
+ [watch]="Starts a livereload server and watches all assets"
+ [test]="Runns unit tests and generates code coverage reports"
+ [release]="Create release versions using git-flow. Usage: mocli release [start|finish] "
+ [release-p]="Create release version and increases patch number"
+ [hotfix]="Create a qa release from current branch and uploads it to bitbucket"
+ [pack]="Zips the dist/ directory. Usage: mocli pack [--production] [--vendor=firefox] [--env=qa]"
+ [help]="Show this message"
+)
+
function printHelp() {
-cat << HELPDOC
- ______________________________________________
- | .__ .__ |
- | _____ ____ ____ | | |__| |
- | / \ / _ \_/ ___\| | | | |
- | | Y Y \| |_) ) \___| |_| | |
- | |__|_| /|____/ \_____>____/__| |
- | \/ Metadefender Cloud CLI |
- | |
- |______________________________________________|
-
- I am your development wizard (0_0)
- Your commands are:
-
- developer - Setup your developer environment
- config - Overwrite common configuration file with specified environment
- Usage: mocli config
- watch - Starts a livereload server and watches all assets
- test - Runns unit tests and generates code coverage reports
- release - Create release versions using git-flow
- Usage: mocli release [start|finish]
- release-p - Create release version and increases patch number
- hotfix - Create a qa release from current branch and uploads it to bitbucket
- pack - Zips the dist/ directory
- Usage: mocli pack [--production] [--vendor=firefox] [--env=qa]
- help - Show this message
-
-HELPDOC
+ echo " ______________________________________________ "
+ echo "| .__ .__ | "
+ echo "| _____ ____ ____ | | |__| | "
+ echo "| / \ / _ \_/ ___\| | | | | "
+ echo "| | Y Y \| |_) ) \___| |_| | | "
+ echo "| |__|_| /|____/ \_____>____/__| | "
+ echo "| \/ MetaDefender Cloud CLI | "
+ echo "| | "
+ echo "|______________________________________________| "
+ echo " "
+ echo "I am your development wizard (0_0)"
+ echo "Your commands are:"
+ echo ""
+ line=" "
+ for i in "${!CMDS_DESC[@]}"
+ do
+ printf "%s %s %s \n" "$i" "${line:${#i}}" "${CMDS_DESC[$i]}"
+ done
+}
+
+function gen_cmp(){
+ for i in "${!CMDS_DESC[@]}"
+ do
+ CMDS+=($i)
+ done
+ mkdir -p ~/.zsh/completion && touch ~/.zsh/completion/_mocli
+ echo -e "#compdef mocli.sh
+ _mocli(){
+ local line
+
+ _arguments -C \
+ \"-h[Show help information]\" \
+ \"--h[Show help information]\" \
+ \"1: :("${CMDS[@]}")\" \
+ \"*::arg:->args\"
+ }
+
+ _mocli" > ~/.zsh/completion/_mocli
}
###
@@ -68,12 +91,18 @@ function copy_secrets() {
echo -e "{\n\t\"googleAnalyticsId\": \"\"\n}" > ./secrets.json
echo -e "\nFailed to copy secrets from s3. Please update ./secrets.json manually.\n"
fi
+}
+function copy_envs() {
echo "-> copy environment specific files"
- aws s3 cp s3://mcl-artifacts/mcl-browser-extension/app/config/local.json ./app/config/ > /dev/null 2>&1
- aws s3 cp s3://mcl-artifacts/mcl-browser-extension/app/config/dev.json ./app/config/ > /dev/null 2>&1
- aws s3 cp s3://mcl-artifacts/mcl-browser-extension/app/config/qa.json ./app/config/ > /dev/null 2>&1
- aws s3 cp s3://mcl-artifacts/mcl-browser-extension/app/config/prod.json ./app/config/ > /dev/null 2>&1
+ ENVS=(local dev qa prod)
+ if [[ "${1}" != "" ]]; then
+ ENVS=(${1})
+ fi
+ for env in ${ENVS[@]}; do
+ echo s3://mcl-artifacts/mcl-browser-extension/app/config/${env}.json
+ aws s3 cp s3://mcl-artifacts/mcl-browser-extension/app/config/${env}.json ./app/config/ > /dev/null 2>&1
+ done
}
if [[ $# == 0 ]]; then
@@ -86,6 +115,7 @@ while [[ $# -gt 0 ]]; do
developer)
# copy the google analytics ID. Create this file manually if you don't have access
copy_secrets
+ copy_envs
echo "-> install gulp-cli"
npm list -g --depth=0 gulp-cli > /dev/null 2>&1 || npm i -g gulp-cli > /dev/null
@@ -99,9 +129,19 @@ while [[ $# -gt 0 ]]; do
echo "-> generate config: prod"
gulp config --prod > /dev/null
+ gen_cmp
+
echo -e "-> Done\n"
exit 0
;;
+ copy-secrets)
+ copy_secrets
+ exit 0
+ ;;
+ copy-envs)
+ copy_envs ${2}
+ exit 0
+ ;;
config)
TOKEN_OK=`in_array "${2}" "${ENVS[@]}"`
if [[ ${TOKEN_OK} = 0 ]]; then
@@ -233,6 +273,9 @@ while [[ $# -gt 0 ]]; do
done
${CMD}
;;
+ gen-cmp)
+ gen_cmp
+ ;;
help)
printHelp
exit 0
diff --git a/package.json b/package.json
index ee05967..f6cecb9 100644
--- a/package.json
+++ b/package.json
@@ -84,6 +84,7 @@
"karma-spec-reporter": "0.0.32",
"karma-webpack": "3.0.5",
"require-dir": "1.2.0",
+ "sinon": "^7.3.2",
"sinon-chrome": "3.0.1",
"standard": "12.0.1",
"uglifyjs-webpack-plugin": "1.3.0",