├── .babelrc ├── .github ├── ISSUE_TEMPLATE │ ├── 1-bug-report.md │ └── 2-feature-request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── README.md ├── configs ├── codecov │ └── codecov.yml ├── electron │ └── builder.js ├── esdoc │ └── esdoc.json ├── eslint │ ├── eslintignore │ └── eslintrc.js ├── flow │ ├── flowconfig │ └── flowcoverage.json ├── jest │ ├── fileTransform.js │ └── jest.config.js ├── lintstaged │ └── lintstaged.json ├── minio │ ├── dev-app-update.yml │ ├── minio.sh │ └── setup.sh ├── parcel │ └── bundler.js ├── prettier │ └── prettierignore ├── sentry │ ├── sentry-symbols.js │ ├── sentryclirc │ └── upload-symbols.js └── stylelint │ └── stylelint.config.js ├── docs ├── CODE_OF_CONDUCT.md ├── LICENSE.md └── SECURITY.md ├── heroku ├── .gitignore ├── README.md ├── index.js ├── package-lock.json ├── package.json └── tasks.json ├── package-lock.json ├── package.json ├── prettier.config.js ├── resources ├── activity.png ├── build │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── icons │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ ├── 64x64.png │ │ └── 96x96.png ├── dashboard.png ├── settings.png ├── virgo-full.logoist └── virgo.logoist ├── scripts └── install_docker.sh └── src ├── main ├── __tests__ │ ├── app.js │ └── main.test.js ├── addons.js ├── api │ ├── docker.js │ └── ipc.js ├── main.js ├── store.js ├── tray.js ├── updater.js └── windows │ └── main.js ├── renderer ├── components │ ├── CrashPage.jsx │ ├── DarkmodeSwitch.jsx │ ├── Error │ │ ├── ErrorBoundary.jsx │ │ └── ErrorDialog.jsx │ ├── LogoIcon.jsx │ └── TimeCounter.jsx ├── containers │ ├── Activity │ │ ├── DockerContainers.jsx │ │ ├── DockerImages.jsx │ │ ├── Table │ │ │ ├── TableHead.jsx │ │ │ ├── TableToolbar.jsx │ │ │ └── helpers.js │ │ └── index.jsx │ ├── App.jsx │ ├── ContentRoutes.jsx │ ├── Dashboard │ │ ├── RunTimer.jsx │ │ ├── TaskInfo.jsx │ │ └── index.jsx │ ├── Layout │ │ ├── MenuBar.jsx │ │ ├── SideDrawer.jsx │ │ ├── SideDrawerList.jsx │ │ ├── StatusBar.jsx │ │ └── TitleBar.jsx │ ├── Preferences │ │ ├── Appearance.jsx │ │ ├── Backends │ │ │ └── FuzzManager.jsx │ │ ├── Docker.jsx │ │ └── index.jsx │ └── Themes │ │ ├── Base.js │ │ ├── Dark.js │ │ ├── Light.js │ │ ├── ThemeProvider.jsx │ │ └── Vibrancy.js ├── fonts │ ├── Roboto-Italic-webfont.woff │ ├── Roboto-Light-webfont.woff │ ├── Roboto-LightItalic-webfont.woff │ ├── Roboto-Medium-webfont.woff │ ├── Roboto-MediumItalic-webfont.woff │ └── Roboto-Regular-webfont.woff ├── images │ ├── test.png │ ├── virgo-full.svg │ └── virgo.svg ├── index.html ├── index.jsx ├── lib │ └── validators.js ├── store │ ├── actions │ │ ├── docker.js │ │ ├── index.js │ │ └── preferences.js │ ├── index.js │ └── reducers │ │ ├── docker.js │ │ ├── index.js │ │ └── preferences.js └── styles │ └── fontface-roboto.css └── shared ├── common.js ├── docker.js ├── file.js ├── fuzzmanager.js ├── logger.js ├── monitor.js ├── sentry.js └── serializers.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | "@babel/preset-flow", 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "electron": "5.0" 10 | } 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "babel-plugin-styled-components", 16 | ["@babel/plugin-proposal-class-properties", { "loose": false }], 17 | "dynamic-import-node" 18 | ], 19 | "env": { 20 | "production": { 21 | "sourceMaps": false, 22 | "retainLines": false 23 | }, 24 | "development": { 25 | "sourceMaps": true, 26 | "retainLines": true 27 | }, 28 | "test": { 29 | "sourceMaps": true, 30 | "retainLines": true 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve Virgo. 4 | --- 5 | 6 | 10 | 11 | - **App Version**: 12 | - **App Platform**: 13 | - **Docker Engine**: 14 | ``` 15 | // Run `docker version` on your Terminal 16 | ``` 17 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest an idea for this project. 4 | --- 5 | 6 | 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | Please describe the problem you are trying to solve. 13 | 14 | **Describe the solution you'd like.** 15 | Please describe the desired behavior. 16 | 17 | **Describe alternatives you've considered.** 18 | Please describe alternative solutions or features you have considered. 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ##### Checklist 8 | 9 | 10 | - [ ] `npm run test` and `npm run lint` passes 11 | - [ ] Tests are included 12 | - [ ] Documentation is changed or added 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Generated 5 | /build 6 | /.cache 7 | /coverage 8 | *.map 9 | *.log 10 | .electron-symbols 11 | .minio 12 | 13 | # Misc 14 | .DS_Store 15 | 16 | # Configs 17 | configs/sentry/sentryclirc 18 | configs/electron/certs 19 | configs/minio/minio.sh 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # @format 2 | language: node_js 3 | node_js: node 4 | env: 5 | global: 6 | - ELECTRON_CACHE=$HOME/.cache/electron 7 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 8 | cache: 9 | npm: true 10 | directories: 11 | - $ELECTRON_CACHE 12 | - $ELECTRON_BUILDER_CACHE 13 | install: 14 | - npm install --silent 15 | # MacOS code signing works only on MacOS. If native dependencies exist and 16 | # pre-builts do not, the release must be built on each platform separately. 17 | jobs: 18 | include: 19 | - stage: Test 20 | script: 21 | - npm run lint || true 22 | - xvfb-run npm run test:coverage 23 | - npm run test:coverage:upload 24 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "msjsdiag.debugger-for-chrome", 4 | "dbaeumer.vscode-eslint", 5 | "shinnn.stylelint", 6 | "donjayamanne.githistory", 7 | "ms-vscode.github-issues-prs", 8 | "eamodio.gitlens", 9 | "wix.vscode-import-cost", 10 | "eg2.vscode-npm-script", 11 | "esbenp.prettier-vscode", 12 | "christian-kohler.npm-intellisense", 13 | "pflannery.vscode-versionlens", 14 | "msjsdiag.vscode-react-native", 15 | "alefragnani.project-manager", 16 | "formulahendry.code-runner" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main via NPM", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "auto", 9 | "env": { 10 | "ELECTRON_START_URL": "http://localhost:3000", 11 | "NODE_ENV": "development", 12 | "PARCEL_WORKERS": "1" 13 | }, 14 | "port": 9222, 15 | "autoAttachChildProcesses": true, 16 | "cwd": "${workspaceFolder}", 17 | "smartStep": true, 18 | "timeout": 100000, 19 | "runtimeExecutable": "npm", 20 | "runtimeArgs": ["run-script", "debug:launch"], 21 | "outFiles": ["${workspaceFolder}/build/app/main/**/*.js", "${workspaceFolder}/src/renderer/**/*.js"], 22 | "sourceMaps": true, 23 | "sourceMapPathOverrides": { 24 | "*": "${workspaceFolder}/src/main/*" 25 | }, 26 | "skipFiles": ["/**/*.js", "${workspaceFolder}/node_modules/**/*.js"] 27 | }, 28 | { 29 | "name": "Electron: Main (Launch)", 30 | "type": "node", 31 | "request": "launch", 32 | "protocol": "auto", 33 | "env": { 34 | "ELECTRON_START_URL": "http://localhost:3000", 35 | "NODE_ENV": "development", 36 | "PARCEL_WORKERS": "1" 37 | }, 38 | "restart": true, 39 | "autoAttachChildProcesses": true, 40 | "cwd": "${workspaceFolder}", 41 | "smartStep": true, 42 | "timeout": 100000, 43 | "preLaunchTask": "Build and Launch Parcel Server", 44 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 45 | "runtimeArgs": ["--nolazy", "--enable-logging", "--remote-debugging-port=9223", "."], 46 | "windows": { 47 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 48 | }, 49 | "outFiles": ["${workspaceFolder}/build/app/main/**/*.js", "${workspaceFolder}/src/renderer/**/*.js"], 50 | "sourceMaps": true, 51 | "sourceMapPathOverrides": { 52 | "*": "${workspaceFolder}/src/main/*" 53 | }, 54 | "skipFiles": ["/**/*.js", "${workspaceFolder}/node_modules/**/*.js"] 55 | }, 56 | { 57 | "name": "Electron: Main (Attach)", 58 | "type": "node", 59 | "request": "attach", 60 | "protocol": "auto", 61 | "port": 9222, // Requires --inspect-port=9222 or --inspect-brk=9222 62 | "restart": true, 63 | "cwd": "${workspaceFolder}", 64 | "smartStep": true, 65 | "timeout": 100000, 66 | "outFiles": ["${workspaceFolder}/build/app/main/**/*.js", "${workspaceFolder}/src/renderer/**/*.js"], 67 | "sourceMaps": true, 68 | "sourceMapPathOverrides": { 69 | "*": "${workspaceFolder}/src/main/*" 70 | }, 71 | "skipFiles": ["/**/*.js", "${workspaceFolder}/node_modules/**/*.js"] 72 | }, 73 | { 74 | "name": "Electron: Renderer", // Requires CMD+R 75 | "type": "chrome", 76 | "request": "attach", 77 | "trace": true, // Path is printed on first line in Debug Console. 78 | "port": 9223, // The --remote-debugging-port provided to electron. 79 | "smartStep": true, 80 | "disableNetworkCache": true, 81 | "timeout": 100000, 82 | "url": "http://127.0.0.1:3000/#/", 83 | "webRoot": "${workspaceFolder}/build/app/renderer/development", 84 | "sourceMaps": true, 85 | "sourceMapPathOverrides": { 86 | // Use the ".scripts" command in the Debug Console to verify the mapped path. 87 | "../../../*": "${workspaceFolder}/*" 88 | }, 89 | "pathMapping": { 90 | "../../../*": "${workspaceFolder}/*" 91 | }, 92 | "skipFiles": ["/**/*.js", "${workspaceFolder}/node_modules/**/*.js"] 93 | } 94 | ], 95 | "compounds": [ 96 | { 97 | "name": "Electron: Combined", 98 | "configurations": ["Electron: Main via NPM", "Electron: Renderer"] 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Hide items in VSCode Explorer. */ 3 | "files.exclude": { 4 | "node_modules": true, 5 | ".cache": true, 6 | "**/.DS_Store": true, 7 | "**/.git": true, 8 | "CODE_OF_CONDUCT.md": true, 9 | "LICENSE.md": true 10 | }, 11 | "files.associations": { 12 | "*.jsx": "javascriptreact" 13 | }, 14 | "files.autoSave": "onFocusChange", 15 | 16 | /* Prettier */ 17 | "prettier.ignorePath": "configs/prettier/prettierignore", 18 | "prettier.disableLanguages": ["javascript", "javascriptreact"], 19 | "editor.rulers": [100], // .prettierrc printWidth setting does somehow not apply. 20 | 21 | /* ESLint */ 22 | "eslint.options": { 23 | "configFile": "configs/eslint/eslintrc.js", 24 | "ignorePath": "configs/eslint/eslintignore" 25 | }, 26 | "eslint.autoFixOnSave": true, 27 | "editor.formatOnSave": true, 28 | "[javascript]": { 29 | "editor.formatOnSave": false 30 | }, 31 | "[javascriptreact]": { 32 | "editor.formatOnSave": false 33 | }, 34 | "javascript.format.enable": false, // Disable default JavaScript formatter in favour of ESLint --fix. 35 | "eslint.validate": ["javascript", "javascriptreact"], 36 | "eslint.alwaysShowStatus": true, 37 | "eslint.packageManager": "npm", 38 | 39 | /* Stylelint */ 40 | "stylelint.enable": true, 41 | "stylelint.config": { 42 | "extends": ["./configs/stylelint/stylelint.config.js"] 43 | }, 44 | "css.validate": false, 45 | "less.validate": false, 46 | "scss.validate": false, 47 | 48 | /* JavaScript */ 49 | "javascript.suggest.completeFunctionCalls": true, 50 | "javascript.referencesCodeLens.enabled": true, 51 | 52 | /* Flow */ 53 | "flow.pathToFlow": "node_modules/.bin/flow", 54 | "javascript.validate.enable": false, // Disable default JavaScript validation in favour of Flow. 55 | 56 | /* Jest */ 57 | "jest.autoEnable": false, // Disable auto-run of Jest cause of "jest-runner/electron" issues. 58 | 59 | /* Misc */ 60 | "debug.inlineValues": false, 61 | "search.runInExtensionHost": true 62 | } 63 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build and Launch Parcel Server", 6 | "type": "shell", 7 | "command": "npm run parcel:main && npm run parcel:launch", 8 | "options": { 9 | "cwd": "${workspaceFolder}" 10 | }, 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "owner": "custom", 14 | "pattern": { 15 | "regexp": "" 16 | }, 17 | "background": { 18 | "activeOnStart": true, 19 | "beginsPattern": "", 20 | "endsPattern": "" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Community Participation Guidelines 4 | 5 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 6 | For more details, please read the 7 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 8 | 9 | ## How to Report 10 | 11 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 12 | 13 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 |
5 | Crowdsourced fuzzing cluster. 🚀 6 |

7 | 8 |

9 | Current Release 10 |

11 | 12 | ## Overview 13 | 14 | - [What is Virgo?](#What-Is-Virgo?) 15 | - [How does it work?](#How-Does-It-Work?) 16 | - [Usage](#Usage) 17 | - [Issues](#Issues) 18 | - [Contributing](#Contributing) 19 | - [Screenshots](#Screenshots) 20 | 21 | ## 🤔What is Virgo? 22 | 23 | [Virgo](https://en.wikipedia.org/wiki/Virgo_Supercluster) is a concept for creating a cluster of fuzzers made by users who are willing to trade and contribute their CPU resources of their workstations to a greater good. 24 | 25 | **Use Cases** 26 | 27 | - Intracompany fuzz testing by using office workstations after-hours without additional spending on cloud providers. 28 | - Software developers can point Virgo to their own Task Definition Server and quickly test among colleagues newly developed features. 29 | - Bug bounty hunters and open source supporters creating a collective supercluster in testing features more quickly and more intensively, and potentially get rewarded for providing their CPU time. 30 | 31 | Virgo can theoretically be used for any arbitrary work task defined in a container. However, Virgo was built as a fuzzing solution in mind. 32 | 33 | ## 💡How does it work? 34 | 35 | Virgo's infrastructure is based on Docker. Virgo fetches routinely a remote server for new tasks by downloading a [Task Definition File](https://virgo-tasks.herokuapp.com/tasks) which contains information on how to run a task and which host preferences are required. 36 | If the required hosts preferences meet the constraints for a certain task, Virgo will download the image, create a container and run that container until a user action intervenes or run "indefinitely". An intervention can be pause, stop, a scheduler, observed system or network activity. If a crash is found during a run, it immediately is sent to our backend for further analyzation and in case of a security issue, you will be informed by the provided contact email address. 37 | 38 | ## 🚀Usage 39 | 40 | Virgo is in its beta stage, obscure bugs may occur. We urge you to file these in our GitHub [issue tracker](https://github.com/MozillaSecurity/virgo/issues) along with any suggestions or feature requests you might have. 41 | 42 | You need to have the Docker engine installed and running on your computer. If you do not have it installed, here are some quick steps to get ready quickly. 43 | 44 | ### Preparation 45 | 46 | #### MacOS 47 | 48 | ``` 49 | brew cask install docker 50 | ``` 51 | 52 | Alternatively: https://download.docker.com/mac/stable/Docker.dmg 53 | 54 | #### Windows 55 | 56 | ``` 57 | choco install docker-desktop 58 | ``` 59 | 60 | Alternatively: https://download.docker.com/win/stable/Docker%20for%20Windows%20Installer.exe 61 | 62 | > Make sure that in `Settings -> Shared Drives` the Volume is enabled on which you installed Virgo. 63 | 64 | #### Linux 65 | 66 | ``` 67 | ./scripts/install_docker.sh 68 | ``` 69 | 70 | > Do not forget to log out/in after this step. 71 | 72 | To make sure Docker is setup and running, you can run the following command: `docker run hello-world`. If you see the `"Hello from Docker!"` message after some seconds, you are good to go. 73 | 74 | ### Launch 75 | 76 | You can now launch Virgo which you downloaded for your platform from the release page. 77 | 78 | > Note: If you are on Linux run `chmod a+x virgo*.AppImage` before you try to launch it. 79 | 80 | If you want to get notified about found security issues discovered by your machine, provide your email address in the Preferences. This step is optional but if your workstation has found a security related issue we will add you to the Bugzilla report to get notified. 81 | 82 | ## Issues 83 | 84 | In case of abnormal behaviour of the application you can reset Virgo to factory default settings. 85 | 86 | ### Common 87 | 88 | #### MacOS 89 | 90 | ``` 91 | rm ~/Library/Application\ Support/virgo/config.json 92 | ``` 93 | 94 | #### Windows 95 | 96 | ``` 97 | rm ~\AppData\Roaming\virgo\config.json 98 | ``` 99 | 100 | #### Linux 101 | 102 | ``` 103 | rm ~/.config/virgo 104 | ``` 105 | 106 | If a task was still running before you closed Virgo, then you can find the task in the `Activity` tab, where you can manually stop and delete it. 107 | 108 | ### Debugging 109 | 110 | If you want to take a glimpse in what is happening under the hood, go to the `Activity` tab and copy the container id of the running task. You can then in the Terminal run: `docker logs --follow` to see what is happening. 111 | 112 | ## Contributing 113 | 114 | ### Launch Virgo 115 | 116 | ``` 117 | git clone https://github.com/mozillasecurity/virgo && cd virgo && npm -s install 118 | npm start 119 | ``` 120 | 121 | > Developer extensions are enabled in non-production builds, except Devtron for analyzing IPC traffic. You can enable it by entering `require('devtron').install()` in the Developer Console. 122 | 123 | To produce a production build run first `npm run build` and optionally `npm run release `. Where `platform` can be `macos64`, `windows64`, `linux64` or `''` to create a release for all platforms. 124 | 125 | For a detailed list of commands run `npm run` 126 | 127 | ### Launch Task Definition Server 128 | 129 | ``` 130 | cd heroku && npm -s install && npm start 131 | ``` 132 | 133 | > You will need to point Virgo to your custom Task Definition Server in the Preferences. 134 | 135 | See [Wiki](https://github.com/MozillaSecurity/virgo/wiki) for detailed setup instructions including Minio for testing in-app updates, Sentry.io for in-app crashes and FuzzManager as custom crash collector backend. 136 | 137 | ## Screenshots 138 | 139 | ![Dashboard](resources/dashboard.png) 140 | ![Activity](resources/activity.png) 141 | ![Settings](resources/settings.png) 142 | 143 | ## Author 144 | 145 | 👤 **Christoph Diehl <cdiehl@mozilla.com>** 146 | 147 | - Twitter: [@posidron](https://twitter.com/posidron) 148 | - Github: [@posidron](https://github.com/posidron) 149 | -------------------------------------------------------------------------------- /configs/codecov/codecov.yml: -------------------------------------------------------------------------------- 1 | # @format 2 | parsers: 3 | javascript: 4 | enable_partials: yes 5 | coverage: 6 | status: 7 | project: 8 | default: false 9 | main: 10 | paths: 11 | - src/main/ 12 | renderer: 13 | paths: 14 | - src/renderer/ 15 | -------------------------------------------------------------------------------- /configs/electron/builder.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const builder = require('electron-builder') 4 | const appRoot = require('app-root-path') 5 | 6 | // eslint-disable-next-line import/no-dynamic-require 7 | const appInfo = require(appRoot.resolve('./package.json')) 8 | 9 | const { Platform, Arch } = builder 10 | const userConfig = { 11 | /* eslint-disable no-template-curly-in-string */ 12 | productName: appInfo.name.charAt(0).toUpperCase() + appInfo.name.slice(1), 13 | appId: `org.mozilla.${appInfo.name}`, 14 | copyright: 'Copyright © 2019 ${author}', 15 | artifactName: '${name}-${version}-${os}-${arch}.${ext}', 16 | files: ['build/app/**/*', 'resources/build/'], 17 | directories: { 18 | output: 'build/releases/${os}/${arch}', 19 | buildResources: 'resources/build' 20 | }, 21 | compression: 'normal', 22 | publish: [ 23 | { 24 | provider: 'github' 25 | } 26 | ], 27 | snap: { 28 | confinement: 'classic', 29 | grade: 'devel' 30 | }, 31 | nsis: { 32 | oneClick: false, 33 | perMachine: true, 34 | allowToChangeInstallationDirectory: true, 35 | createDesktopShortcut: 'always', 36 | deleteAppDataOnUninstall: true 37 | }, 38 | win: {}, 39 | mac: { 40 | category: 'public.app-category.security' 41 | }, 42 | linux: { 43 | synopsis: appInfo.description, 44 | executableName: appInfo.name, 45 | category: 'Security' 46 | } 47 | } 48 | 49 | /* To test the update process in packaged production builds, locally via Minio. */ 50 | if (process.env.APP_TEST_PUBLISHER === 'S3') { 51 | userConfig.publish = [ 52 | { 53 | provider: 's3', 54 | bucket: 'electron-builder', 55 | endpoint: 'http://127.0.0.1:9000' 56 | } 57 | ] 58 | } 59 | 60 | const main = async () => { 61 | let targetArgs 62 | if (process.env.NODE_ENV === 'production') { 63 | targetArgs = { 64 | windows64: Platform.WINDOWS.createTarget(['nsis'], Arch.x64), 65 | linux64: Platform.LINUX.createTarget([/* 'snap' */ 'AppImage'], Arch.x64), 66 | /* 67 | * MacOS needs zip for updating. 68 | * Generating DMGs on MacOS Catalina is not possible currently due to abadonment of 32bit support. 69 | */ 70 | macos64: Platform.MAC.createTarget(['zip', 'dmg' /* 'pkg' */], Arch.x64) 71 | } 72 | } else { 73 | targetArgs = { 74 | windows64: Platform.WINDOWS.createTarget(['dir'], Arch.x64), 75 | linux64: Platform.LINUX.createTarget(['dir'], Arch.x64), 76 | macos64: Platform.MAC.createTarget(['dir'], Arch.x64) 77 | } 78 | } 79 | 80 | let targets = process.argv.filter(arg => !!targetArgs[arg]).map(arg => targetArgs[arg]) 81 | if (targets.length === 0) { 82 | targets = Object.keys(targetArgs).map(arg => targetArgs[arg]) 83 | } 84 | 85 | console.log(`Building on platform ${process.platform} in ${process.env.NODE_ENV} mode.`) 86 | // eslint-disable-next-line no-restricted-syntax 87 | for (const target of targets) { 88 | const config = JSON.parse(JSON.stringify(userConfig)) 89 | // eslint-disable-next-line no-await-in-loop 90 | await builder 91 | .build({ targets: target, config }) 92 | .then(files => { 93 | console.log('🙌 Build is OK!') 94 | files.map(file => console.log(`• ${file}`)) 95 | }) 96 | .catch(error => { 97 | console.log(`🔥 Build is toast!\n${error}`) 98 | }) 99 | } 100 | } 101 | 102 | main() 103 | -------------------------------------------------------------------------------- /configs/esdoc/esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./build/docs", 4 | "includes": [".*\\.js", ".*\\.jsx"], 5 | "package": "./package.json", 6 | "index": "./README.md", 7 | "plugins": [ 8 | { 9 | "name": "esdoc-standard-plugin", 10 | "option": { 11 | "lint": { "enable": true }, 12 | "coverage": { "enable": true } 13 | } 14 | }, 15 | { "name": "esdoc-jsx-plugin" }, 16 | { "name": "esdoc-node" }, 17 | { "name": "esdoc-ecmascript-proposal-plugin", "option": { "all": true } } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /configs/eslint/eslintignore: -------------------------------------------------------------------------------- 1 | resources/ 2 | build/ 3 | flow-typed/ 4 | -------------------------------------------------------------------------------- /configs/eslint/eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /* 4 | * Changes to the configuration require a restart in VSCode to be effective. 5 | */ 6 | 7 | const pkg = require('../../package.json') 8 | 9 | const version = dependency => { 10 | if (pkg.dependencies && pkg.dependencies[dependency]) { 11 | return pkg.dependencies[dependency].replace(/[^0-9.]/g, '') 12 | } 13 | if (pkg.devDependencies && pkg.devDependencies[dependency]) { 14 | return pkg.devDependencies[dependency].replace(/[^0-9.]/g, '') 15 | } 16 | console.log(`Unable to find dependency in package.json for "${dependency}".`) 17 | return undefined 18 | } 19 | 20 | /* New base configuration. */ 21 | module.exports = { 22 | parserOptions: { 23 | ecmaVersion: 2018, 24 | sourceType: 'module', 25 | ecmaFeatures: { 26 | jsx: true 27 | } 28 | }, 29 | env: { 30 | jest: true, 31 | node: true, 32 | browser: true 33 | }, 34 | parser: 'babel-eslint', 35 | extends: [ 36 | // Base configuration. 37 | 'airbnb', 38 | 'plugin:jest/recommended', 39 | 'plugin:flowtype/recommended', 40 | 'plugin:prettier/recommended', 41 | // 'prettier/flowtype', // Causes issues with prettier.printWidth 42 | 'prettier/react' 43 | ], 44 | plugins: ['react', 'jest', 'flowtype', 'prettier'], 45 | settings: { 46 | flowtype: { 47 | onlyFilesWithFlowAnnotation: false 48 | }, 49 | react: { 50 | version: 'detect', 51 | flowVersion: version('flow-bin') 52 | }, 53 | 'import/core-modules': ['electron', 'electron-builder', 'electron-devtools-installer'] 54 | }, 55 | rules: { 56 | // Add rules for above plugins. 57 | 'no-console': 0 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /configs/flow/flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.*/test 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [lints] 9 | 10 | [options] 11 | emoji=true 12 | module.file_ext=.js 13 | module.file_ext=.jsx 14 | module.file_ext=.css 15 | 💩 https://github.com/facebook/flow/issues/338 16 | module.file_ext=.scss 17 | module.file_ext=.json 18 | 19 | [strict] 20 | -------------------------------------------------------------------------------- /configs/flow/flowcoverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "concurrentFiles": 2, 3 | "globExcludePatterns": ["node_modules/**"], 4 | "globIncludePatterns": ["src/**/*.{js,jsx}"], 5 | "outputDir": "build/coverage/flow", 6 | "threshold": 70, 7 | "reportTypes": ["html", "text"] 8 | } 9 | -------------------------------------------------------------------------------- /configs/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const path = require('path') 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};` 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /configs/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const { defaults } = require('jest-config') 4 | 5 | module.exports = { 6 | /* See `@jest/types/build/Config.d.ts` for what is valid in global & project config or 7 | * provide references to separate configuration files to the project's array rather than 8 | * an array of configuration objects. 9 | */ 10 | verbose: true, 11 | rootDir: '../../', 12 | coverageDirectory: '/build/coverage', 13 | collectCoverageFrom: ['src/**/*.js?(x)'], 14 | coverageReporters: ['json', 'html', 'text', 'lcov'], 15 | transform: { 16 | '^.+\\.jsx?$': 'babel-jest', 17 | '\\.(jpg|png|gif|eot|otf|webp|svg|ttf|woff|mp4|webm|wav|mp3|m4a|aac|oga|css|less)$': 18 | '/configs/jest/fileTransform.js' 19 | }, 20 | globals: { 21 | 'babel-jest': { 22 | useBabelrc: true 23 | } 24 | }, 25 | moduleFileExtensions: [...defaults.moduleFileExtensions], 26 | projects: [ 27 | { 28 | rootDir: '.', 29 | displayName: 'Main', 30 | runner: '@jest-runner/electron/main', 31 | testEnvironment: 'node', 32 | testMatch: ['**/main/__tests__/**/*.(spec|test).js'] 33 | }, 34 | { 35 | rootDir: '.', 36 | displayName: 'Renderer', 37 | runner: '@jest-runner/electron', 38 | testEnvironment: '@jest-runner/electron/environment', 39 | testMatch: ['**/renderer/__tests__/**/*.(spec|test).jsx'] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /configs/lintstaged/lintstaged.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx}": ["npm run eslint -- --fix", "git add"], 3 | "*.{css,scss}": ["npm run stylelint -- --fix", "git add"], 4 | "*.{json,md}": ["npm run prettier -- --write", "git add"] 5 | } 6 | -------------------------------------------------------------------------------- /configs/minio/dev-app-update.yml: -------------------------------------------------------------------------------- 1 | # @format 2 | 3 | # To prevent exception raising about missing `dev-app-update.yml` during `npm start`. 4 | # We do not want to disable the updater for development environment in order to do UI work 5 | # on the updater. In order to test the updater in production packaged environments 6 | # set `APP_TEST_PUBLISHER=S3`. 7 | provider: 's3' 8 | bucket: 'electron-builder' 9 | endpoint: 'http://127.0.0.1:9000' 10 | -------------------------------------------------------------------------------- /configs/minio/minio.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # For local development and testing packaged production builds only! 5 | # 6 | 7 | export NODE_ENV="production" 8 | export APP_TEST_PUBLISHER="S3" 9 | export AWS_ACCESS_KEY_ID="" 10 | export AWS_SECRET_ACCESS_KEY="" 11 | 12 | mc config host add electron-builder http://127.0.0.1:9000 $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY 13 | mc mb electron-builder/electron-builder 14 | 15 | rm -rf build 16 | npm run build 17 | npm run release macos64 18 | -------------------------------------------------------------------------------- /configs/minio/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | minio server .minio 4 | 5 | -------------------------------------------------------------------------------- /configs/parcel/bundler.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | // https://github.com/parcel-bundler/parcel/issues/1005#issuecomment-419688410 4 | 5 | const Bundler = require('parcel-bundler') 6 | const Server = require('express')() 7 | const appRoot = require('app-root-path') 8 | 9 | const options = { 10 | outDir: appRoot.resolve('build/app/renderer/development'), // The out directory to put the build files in, defaults to dist 11 | outFile: 'index.html', // The name of the outputFile 12 | // publicUrl: './', // The url to server on, defaults to dist 13 | watch: true, // whether to watch the files and rebuild them on change, defaults to process.env.NODE_ENV !== 'production' 14 | cache: true, // Enabled or disables caching, defaults to true 15 | cacheDir: appRoot.resolve('.cache'), // The directory cache gets put in, defaults to .cache 16 | contentHash: false, // Disable content hash from being included on the filename 17 | minify: false, // Minify files, enabled if process.env.NODE_ENV === 'production' 18 | scopeHoist: false, // turn on experimental scope hoisting/tree shaking flag, for smaller production bundles 19 | target: 'electron', // browser/node/electron, defaults to browser 20 | https: false, // Serve files over https or http, defaults to false 21 | logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors 22 | hmrPort: 0, // The port the HMR socket runs on, defaults to a random free port (0 in node.js resolves to a random free port) 23 | sourceMaps: true, // Enable or disable sourcemaps, defaults to enabled (not supported in minified builds yet) 24 | hmrHostname: '', // A hostname for hot module reload, default to '' 25 | detailedReport: false // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled 26 | } 27 | 28 | const runBundle = async (entrypoint, port) => { 29 | // Initializes a bundler using the entrypoint location and options provided. 30 | const bundler = new Bundler(entrypoint, options) 31 | 32 | // Let express use the bundler middleware, this will let Parcel handle every request over your express server. 33 | Server.use(bundler.middleware()) 34 | Server.listen(port) 35 | 36 | // Run the bundler, this returns the main bundle. 37 | // Use the events if you're using watch mode as this promise will only trigger once and not for every rebuild. 38 | await bundler.bundle() 39 | } 40 | 41 | runBundle(appRoot.resolve('src/renderer/index.html'), 3000) 42 | -------------------------------------------------------------------------------- /configs/prettier/prettierignore: -------------------------------------------------------------------------------- 1 | packages-lock.json 2 | flow-typed/ 3 | build/ 4 | .cache/ 5 | .github/ 6 | -------------------------------------------------------------------------------- /configs/sentry/sentry-symbols.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable import/no-extraneous-dependencies */ 4 | /* eslint-disable import/no-dynamic-require */ 5 | 6 | const appRoot = require('app-root-path') 7 | 8 | let SentryCli 9 | let download 10 | try { 11 | SentryCli = require('@sentry/cli') 12 | download = require('electron-download') 13 | } catch (e) { 14 | console.error('ERROR: Missing required packages, please run:') 15 | console.error('npm install --save-dev @sentry/cli electron-download') 16 | process.exit(1) 17 | } 18 | 19 | const VERSION = /\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/i 20 | const SYMBOL_CACHE_FOLDER = appRoot.resolve('.electron-symbols') 21 | const PACKAGE = require(appRoot.resolve('./package.json')) 22 | const sentryCli = new SentryCli() 23 | 24 | function getElectronVersion() { 25 | if (!PACKAGE) { 26 | return false 27 | } 28 | 29 | const electronVersion = 30 | (PACKAGE.dependencies && PACKAGE.dependencies.electron) || 31 | (PACKAGE.devDependencies && PACKAGE.devDependencies.electron) 32 | 33 | if (!electronVersion) { 34 | return false 35 | } 36 | 37 | const matches = VERSION.exec(electronVersion) 38 | return matches ? matches[0] : false 39 | } 40 | 41 | async function downloadSymbols(options) { 42 | return new Promise((resolve, reject) => { 43 | download( 44 | { 45 | ...options, 46 | cache: SYMBOL_CACHE_FOLDER 47 | }, 48 | (err, zipPath) => { 49 | if (err) { 50 | reject(err) 51 | } else { 52 | resolve(zipPath) 53 | } 54 | } 55 | ) 56 | }) 57 | } 58 | 59 | async function main() { 60 | const version = getElectronVersion() 61 | if (!version) { 62 | console.error('Cannot detect Electron version, check package.json') 63 | return 64 | } 65 | 66 | console.log('We are starting to download all possible Electron symbols.') 67 | console.log('We need it in order to symbolicate native crashes.') 68 | console.log('This step is only needed once whenever you update your Electron version.') 69 | console.log('Just call this script again it should do everything for you.\n') 70 | 71 | let zipPath = await downloadSymbols({ 72 | version, 73 | platform: 'darwin', 74 | arch: 'x64', 75 | dsym: true 76 | }) 77 | await sentryCli.execute(['upload-dif', '-t', 'dsym', zipPath], true) 78 | 79 | zipPath = await downloadSymbols({ 80 | version, 81 | platform: 'win32', 82 | arch: 'ia32', 83 | symbols: true 84 | }) 85 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true) 86 | 87 | zipPath = await downloadSymbols({ 88 | version, 89 | platform: 'win32', 90 | arch: 'x64', 91 | symbols: true 92 | }) 93 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true) 94 | 95 | zipPath = await downloadSymbols({ 96 | version, 97 | platform: 'linux', 98 | arch: 'x64', 99 | symbols: true 100 | }) 101 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true) 102 | 103 | console.log('Finished downloading and uploading to Sentry.') 104 | console.log(`Feel free to delete the ${SYMBOL_CACHE_FOLDER}.`) 105 | } 106 | 107 | main().catch(error => console.error(error)) 108 | -------------------------------------------------------------------------------- /configs/sentry/sentryclirc: -------------------------------------------------------------------------------- 1 | # NOTE: Run `source ./configs/sentry/sentryclirc` before any Sentry tool. 2 | # 3 | # https://gist.github.com/DarrenN/8c6a5b969481725a4413 4 | # export PACKAGE_VERSION=$(node -p -e "require('./package.json').version") 5 | 6 | export SENTRY_ORG=mozillasecurity 7 | export SENTRY_PROJECT="virgo" 8 | export SENTRY_URL="https://sentry.io" 9 | export SENTRY_AUTH_TOKEN="" 10 | export SENTRY_LOG_LEVEL=info -------------------------------------------------------------------------------- /configs/sentry/upload-symbols.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable import/no-dynamic-require */ 4 | 5 | const appRoot = require('app-root-path') 6 | 7 | let SentryCli 8 | try { 9 | SentryCli = require('@sentry/cli') 10 | } catch (e) { 11 | console.error('ERROR: Missing required packages, please run:') 12 | console.error('npm install --save-dev @sentry/cli') 13 | process.exit(1) 14 | } 15 | 16 | const PACKAGE = require(appRoot.resolve('./package.json')) 17 | const sentryCli = new SentryCli() 18 | 19 | async function main() { 20 | await sentryCli.execute( 21 | [ 22 | 'releases', 23 | 'files', 24 | `${PACKAGE.name}-${PACKAGE.version}`, 25 | 'upload-sourcemaps', 26 | 'build/app', 27 | '--url-prefix', 28 | '~/build/app', 29 | '--rewrite', 30 | '--validate' 31 | ], 32 | true 33 | ) 34 | } 35 | 36 | main().catch(error => console.error(error)) 37 | -------------------------------------------------------------------------------- /configs/stylelint/stylelint.config.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | module.exports = { 4 | extends: ['stylelint-config-recommended', 'stylelint-prettier/recommended'] 5 | } 6 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Community Participation Guidelines 4 | 5 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 6 | For more details, please read the 7 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 8 | 9 | ## How to Report 10 | 11 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 12 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Security Policy 4 | 5 | To report a security vulnerability, please email [cdiehl@mozilla.com](mailto:cdiehl@mozilla.com). 6 | -------------------------------------------------------------------------------- /heroku/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /heroku/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Virgo Task Definition Server 4 | 5 | Serves task definitions as JSON format. For development purposes run: 6 | 7 | ``` 8 | npm -s install 9 | npm start 10 | ``` 11 | 12 | The official Virgo server is reachable at `https://virgo-tasks.herokuapp.com/tasks` 13 | -------------------------------------------------------------------------------- /heroku/index.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const fs = require('fs') 4 | const express = require('express') 5 | 6 | const app = express() 7 | 8 | const port = process.env.PORT || 443 9 | 10 | app.use(function(req, res, next) { 11 | res.header('Access-Control-Allow-Origin', '*') 12 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') 13 | next() 14 | }) 15 | 16 | app.get('/', (req, res) => res.send('Virgo')) 17 | 18 | app.get('/tasks', function(req, res) { 19 | fs.readFile('./tasks.json', (err, json) => { 20 | const obj = JSON.parse(json) 21 | res.json(obj) 22 | }) 23 | }) 24 | 25 | app.listen(port, () => { 26 | console.log(`Task Definition Server is listening on port ${port}.`) 27 | }) 28 | -------------------------------------------------------------------------------- /heroku/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virgo-tds", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "body-parser": { 22 | "version": "1.19.0", 23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 24 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 25 | "requires": { 26 | "bytes": "3.1.0", 27 | "content-type": "~1.0.4", 28 | "debug": "2.6.9", 29 | "depd": "~1.1.2", 30 | "http-errors": "1.7.2", 31 | "iconv-lite": "0.4.24", 32 | "on-finished": "~2.3.0", 33 | "qs": "6.7.0", 34 | "raw-body": "2.4.0", 35 | "type-is": "~1.6.17" 36 | } 37 | }, 38 | "bytes": { 39 | "version": "3.1.0", 40 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 41 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 42 | }, 43 | "content-disposition": { 44 | "version": "0.5.3", 45 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 46 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 47 | "requires": { 48 | "safe-buffer": "5.1.2" 49 | } 50 | }, 51 | "content-type": { 52 | "version": "1.0.4", 53 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 54 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 55 | }, 56 | "cookie": { 57 | "version": "0.4.0", 58 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 59 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 60 | }, 61 | "cookie-signature": { 62 | "version": "1.0.6", 63 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 64 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 65 | }, 66 | "debug": { 67 | "version": "2.6.9", 68 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 69 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 70 | "requires": { 71 | "ms": "2.0.0" 72 | } 73 | }, 74 | "depd": { 75 | "version": "1.1.2", 76 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 77 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 78 | }, 79 | "destroy": { 80 | "version": "1.0.4", 81 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 82 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 83 | }, 84 | "ee-first": { 85 | "version": "1.1.1", 86 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 87 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 88 | }, 89 | "encodeurl": { 90 | "version": "1.0.2", 91 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 92 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 93 | }, 94 | "escape-html": { 95 | "version": "1.0.3", 96 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 97 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 98 | }, 99 | "etag": { 100 | "version": "1.8.1", 101 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 102 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 103 | }, 104 | "express": { 105 | "version": "4.17.1", 106 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 107 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 108 | "requires": { 109 | "accepts": "~1.3.7", 110 | "array-flatten": "1.1.1", 111 | "body-parser": "1.19.0", 112 | "content-disposition": "0.5.3", 113 | "content-type": "~1.0.4", 114 | "cookie": "0.4.0", 115 | "cookie-signature": "1.0.6", 116 | "debug": "2.6.9", 117 | "depd": "~1.1.2", 118 | "encodeurl": "~1.0.2", 119 | "escape-html": "~1.0.3", 120 | "etag": "~1.8.1", 121 | "finalhandler": "~1.1.2", 122 | "fresh": "0.5.2", 123 | "merge-descriptors": "1.0.1", 124 | "methods": "~1.1.2", 125 | "on-finished": "~2.3.0", 126 | "parseurl": "~1.3.3", 127 | "path-to-regexp": "0.1.7", 128 | "proxy-addr": "~2.0.5", 129 | "qs": "6.7.0", 130 | "range-parser": "~1.2.1", 131 | "safe-buffer": "5.1.2", 132 | "send": "0.17.1", 133 | "serve-static": "1.14.1", 134 | "setprototypeof": "1.1.1", 135 | "statuses": "~1.5.0", 136 | "type-is": "~1.6.18", 137 | "utils-merge": "1.0.1", 138 | "vary": "~1.1.2" 139 | } 140 | }, 141 | "finalhandler": { 142 | "version": "1.1.2", 143 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 144 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 145 | "requires": { 146 | "debug": "2.6.9", 147 | "encodeurl": "~1.0.2", 148 | "escape-html": "~1.0.3", 149 | "on-finished": "~2.3.0", 150 | "parseurl": "~1.3.3", 151 | "statuses": "~1.5.0", 152 | "unpipe": "~1.0.0" 153 | } 154 | }, 155 | "forwarded": { 156 | "version": "0.1.2", 157 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 158 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 159 | }, 160 | "fresh": { 161 | "version": "0.5.2", 162 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 163 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 164 | }, 165 | "http-errors": { 166 | "version": "1.7.2", 167 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 168 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 169 | "requires": { 170 | "depd": "~1.1.2", 171 | "inherits": "2.0.3", 172 | "setprototypeof": "1.1.1", 173 | "statuses": ">= 1.5.0 < 2", 174 | "toidentifier": "1.0.0" 175 | } 176 | }, 177 | "iconv-lite": { 178 | "version": "0.4.24", 179 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 180 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 181 | "requires": { 182 | "safer-buffer": ">= 2.1.2 < 3" 183 | } 184 | }, 185 | "inherits": { 186 | "version": "2.0.3", 187 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 188 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 189 | }, 190 | "ipaddr.js": { 191 | "version": "1.9.0", 192 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 193 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 194 | }, 195 | "media-typer": { 196 | "version": "0.3.0", 197 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 198 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 199 | }, 200 | "merge-descriptors": { 201 | "version": "1.0.1", 202 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 203 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 204 | }, 205 | "methods": { 206 | "version": "1.1.2", 207 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 208 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 209 | }, 210 | "mime": { 211 | "version": "1.6.0", 212 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 213 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 214 | }, 215 | "mime-db": { 216 | "version": "1.40.0", 217 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 218 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 219 | }, 220 | "mime-types": { 221 | "version": "2.1.24", 222 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 223 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 224 | "requires": { 225 | "mime-db": "1.40.0" 226 | } 227 | }, 228 | "ms": { 229 | "version": "2.0.0", 230 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 231 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 232 | }, 233 | "negotiator": { 234 | "version": "0.6.2", 235 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 236 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 237 | }, 238 | "on-finished": { 239 | "version": "2.3.0", 240 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 241 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 242 | "requires": { 243 | "ee-first": "1.1.1" 244 | } 245 | }, 246 | "parseurl": { 247 | "version": "1.3.3", 248 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 249 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 250 | }, 251 | "path-to-regexp": { 252 | "version": "0.1.7", 253 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 254 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 255 | }, 256 | "proxy-addr": { 257 | "version": "2.0.5", 258 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 259 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 260 | "requires": { 261 | "forwarded": "~0.1.2", 262 | "ipaddr.js": "1.9.0" 263 | } 264 | }, 265 | "qs": { 266 | "version": "6.7.0", 267 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 268 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 269 | }, 270 | "range-parser": { 271 | "version": "1.2.1", 272 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 273 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 274 | }, 275 | "raw-body": { 276 | "version": "2.4.0", 277 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 278 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 279 | "requires": { 280 | "bytes": "3.1.0", 281 | "http-errors": "1.7.2", 282 | "iconv-lite": "0.4.24", 283 | "unpipe": "1.0.0" 284 | } 285 | }, 286 | "safe-buffer": { 287 | "version": "5.1.2", 288 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 289 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 290 | }, 291 | "safer-buffer": { 292 | "version": "2.1.2", 293 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 294 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 295 | }, 296 | "send": { 297 | "version": "0.17.1", 298 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 299 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 300 | "requires": { 301 | "debug": "2.6.9", 302 | "depd": "~1.1.2", 303 | "destroy": "~1.0.4", 304 | "encodeurl": "~1.0.2", 305 | "escape-html": "~1.0.3", 306 | "etag": "~1.8.1", 307 | "fresh": "0.5.2", 308 | "http-errors": "~1.7.2", 309 | "mime": "1.6.0", 310 | "ms": "2.1.1", 311 | "on-finished": "~2.3.0", 312 | "range-parser": "~1.2.1", 313 | "statuses": "~1.5.0" 314 | }, 315 | "dependencies": { 316 | "ms": { 317 | "version": "2.1.1", 318 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 319 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 320 | } 321 | } 322 | }, 323 | "serve-static": { 324 | "version": "1.14.1", 325 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 326 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 327 | "requires": { 328 | "encodeurl": "~1.0.2", 329 | "escape-html": "~1.0.3", 330 | "parseurl": "~1.3.3", 331 | "send": "0.17.1" 332 | } 333 | }, 334 | "setprototypeof": { 335 | "version": "1.1.1", 336 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 337 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 338 | }, 339 | "statuses": { 340 | "version": "1.5.0", 341 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 342 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 343 | }, 344 | "toidentifier": { 345 | "version": "1.0.0", 346 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 347 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 348 | }, 349 | "type-is": { 350 | "version": "1.6.18", 351 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 352 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 353 | "requires": { 354 | "media-typer": "0.3.0", 355 | "mime-types": "~2.1.24" 356 | } 357 | }, 358 | "unpipe": { 359 | "version": "1.0.0", 360 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 361 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 362 | }, 363 | "utils-merge": { 364 | "version": "1.0.1", 365 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 366 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 367 | }, 368 | "vary": { 369 | "version": "1.1.2", 370 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 371 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 372 | } 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /heroku/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virgo-tds", 3 | "version": "1.0.0", 4 | "description": "Task Definition Server", 5 | "main": "index.js", 6 | "dependencies": { 7 | "express": "^4.17.1" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "start": "node index.js" 12 | }, 13 | "author": "Christoph Diehl ", 14 | "license": "MPL-2.0" 15 | } 16 | -------------------------------------------------------------------------------- /heroku/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "name": "mozillasecurity/libfuzzer:latest", 5 | "environment": ["FUZZER=ContentParentIPC"] 6 | }, 7 | { 8 | "name": "mozillasecurity/libfuzzer:latest", 9 | "environment": ["TOKENS=dicts/sdp.dict", "CORPORA=samples/sdp/", "FUZZER=SdpParser"] 10 | }, 11 | { 12 | "name": "mozillasecurity/libfuzzer:latest", 13 | "environment": ["TOKENS=dicts/stun.dict", "CORPORA=samples/stun/", "FUZZER=StunParser"] 14 | }, 15 | { 16 | "name": "mozillasecurity/libfuzzer:latest", 17 | "environment": [ 18 | "TOKENS=dicts/qcms.dict", 19 | "CORPORA=samples/icc/profiles/common", 20 | "FUZZER=Qcms" 21 | ] 22 | }, 23 | { 24 | "name": "mozillasecurity/libfuzzer:latest", 25 | "environment": ["FUZZER=NetworkHttp"] 26 | }, 27 | { 28 | "name": "mozillasecurity/libfuzzer:latest", 29 | "environment": ["FUZZER=Dav1dDecode"] 30 | }, 31 | { 32 | "name": "mozillasecurity/libfuzzer:latest", 33 | "environment": [ 34 | "TOKENS=dicts/content_security_policy.dict", 35 | "CORPORA=samples/", 36 | "FUZZER=ContentSecurityPolicyParser" 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virgo", 3 | "version": "0.3.1", 4 | "description": "Virgo - A crowdfuzz approach for the desktop.", 5 | "keywords": [ 6 | "cluster", 7 | "fuzzing", 8 | "utility", 9 | "bounty", 10 | "bugs" 11 | ], 12 | "homepage": "https://github.com/mozillasecurity/virgo", 13 | "bugs": { 14 | "url": "https://github.com/mozillasecurity/virgo/issues" 15 | }, 16 | "license": "MPL-2.0", 17 | "author": "Christoph Diehl ", 18 | "repository": "github:mozillasecurity/virgo", 19 | "private": true, 20 | "engines": { 21 | "node": ">=11" 22 | }, 23 | "main": "build/app/main/main.js", 24 | "scripts": { 25 | "\n# Development": "", 26 | "start": "cross-env NODE_ENV=development run-p -r parcel:launch electron:launch", 27 | "electron:launch": "run-s parcel:main electron:start", 28 | "electron:start": "cross-env ELECTRON_START_URL=http://localhost:3000 electron --enable-logging --remote-debugging-port=9223 .", 29 | "parcel:launch": "node configs/parcel/bundler.js", 30 | "\n# Production": "", 31 | "build": "run-s parcel:main parcel:renderer", 32 | "release": "node configs/electron/builder.js", 33 | "parcel:main": "parcel build src/main/main.js --out-dir build/app/main --out-file=main --target=electron", 34 | "parcel:renderer": "parcel build src/renderer/index.html --log-level 3 --no-cache --public-url ./ --out-dir build/app/renderer/production --target=electron", 35 | "\n# Debug": "", 36 | "debug:electron": "electron --inspect-brk=9222 --enable-logging --remote-debugging-port=9223 .", 37 | "debug:launch": "run-p parcel:main parcel:launch debug:electron", 38 | "\n# Test": "", 39 | "jest": "cross-env NODE_ENV=test CI=true jest --config configs/jest/jest.config.js --colors", 40 | "test": "run-s build jest", 41 | "test:coverage": "run-s build 'jest -- --coverage'", 42 | "test:coverage:upload": "codecov -f build/coverage/coverage-final.json -y configs/codecov/codecov.yml --disable=gcov", 43 | "test:snapshots": "npm run jest -- -u", 44 | "\n# Lint": "", 45 | "eslint": "eslint --config configs/eslint/eslintrc.js --ignore-path configs/eslint/eslintignore --color", 46 | "stylelint": "stylelint --config configs/stylelint/stylelint.config.js --formatter=verbose --color", 47 | "lint": "run-s -c typecheck lint:js lint:css", 48 | "lint:js": "npm run eslint -- --fix 'src/**/*.{js,jsx}'", 49 | "lint:css": "npm run stylelint -- 'src/**/*.{css,scss}' --fix", 50 | "\n# Format": "", 51 | "prettier": "prettier --config prettier.config.js --ignore-path configs/prettier/prettierignore", 52 | "format": "run-s -c format:js format:css", 53 | "format:js": "npm run prettier -- --write 'src/**/*.{js,jsx}'", 54 | "format:css": "npm run prettier -- --write './src/**/*.{css,scss}'", 55 | "\n# Documentation": "", 56 | "docs": "esdoc -c configs/esdoc/esdoc.json", 57 | "\n# Flow": "", 58 | "typecheck": "flow check --quiet --color=always --flowconfig-name configs/flow/flowconfig", 59 | "typecheck:init": "flow-typed install --overwrite", 60 | "typecheck:update": "flow-typed update", 61 | "typecheck:upgrade": "flow-upgrade", 62 | "typecheck:coverage": "flow-coverage-report --config configs/flow/flowcoverage.json", 63 | "\n# Packages": "", 64 | "updates": "ncu -t" 65 | }, 66 | "husky": { 67 | "hooks": { 68 | "pre-commit": "lint-staged --config configs/lintstaged/lintstaged.json" 69 | } 70 | }, 71 | "devDependencies": { 72 | "@babel/core": "^7.5.4", 73 | "@babel/plugin-proposal-class-properties": "^7.5.0", 74 | "@babel/preset-env": "^7.5.4", 75 | "@babel/preset-flow": "^7.0.0", 76 | "@babel/preset-react": "^7.0.0", 77 | "@jest-runner/electron": "2.0.2", 78 | "@sentry/cli": "^1.46.0", 79 | "babel-eslint": "^10.0.2", 80 | "babel-jest": "^24.8.0", 81 | "babel-plugin-dynamic-import-node": "^2.3.0", 82 | "babel-plugin-styled-components": "^1.10.6", 83 | "codecov": "^3.5.0", 84 | "cross-env": "^5.2.0", 85 | "devtron": "^1.4.0", 86 | "electron": "^5.0.7", 87 | "electron-builder": "^21.0.15", 88 | "electron-devtools-installer": "^2.2.4", 89 | "esdoc": "^1.1.0", 90 | "esdoc-ecmascript-proposal-plugin": "^1.0.0", 91 | "esdoc-jsx-plugin": "^1.0.0", 92 | "esdoc-node": "^1.0.4", 93 | "esdoc-standard-plugin": "^1.0.0", 94 | "eslint": "^6.0.1", 95 | "eslint-config-airbnb": "^17.1.1", 96 | "eslint-config-prettier": "^6.0.0", 97 | "eslint-plugin-flowtype": "^3.11.1", 98 | "eslint-plugin-import": "^2.18.0", 99 | "eslint-plugin-jest": "^22.9.0", 100 | "eslint-plugin-jsx-a11y": "^6.2.3", 101 | "eslint-plugin-prettier": "^3.1.0", 102 | "eslint-plugin-react": "^7.14.2", 103 | "express": "^4.17.1", 104 | "flow-bin": "^0.102.0", 105 | "flow-coverage-report": "^0.6.1", 106 | "flow-typed": "^2.6.0", 107 | "husky": "^3.0.0", 108 | "jest": "^24.8.0", 109 | "jest-config": "^24.8.0", 110 | "lint-staged": "^9.2.0", 111 | "npm-check-updates": "^3.1.20", 112 | "npm-run-all": "^4.1.5", 113 | "parcel-bundler": "^1.12.3", 114 | "prettier": "^1.18.2", 115 | "prettier-eslint-cli": "^5.0.0", 116 | "react-test-renderer": "^16.8.6", 117 | "regenerator-runtime": "^0.13.2", 118 | "spectron": "^7.0.0", 119 | "stylelint": "^10.1.0", 120 | "stylelint-config-prettier": "^5.2.0", 121 | "stylelint-config-recommended": "^2.2.0", 122 | "stylelint-prettier": "^1.1.1" 123 | }, 124 | "dependencies": { 125 | "@material-ui/core": "^3.9.3", 126 | "@material-ui/icons": "^3.0.2", 127 | "@mozillasecurity/octo": "^2.0.0", 128 | "@sentry/electron": "^0.17.1", 129 | "app-root-path": "^2.2.1", 130 | "axios": "^0.19.0", 131 | "dockerode": "^2.5.8", 132 | "electron-log": "^3.0.6", 133 | "electron-store": "^4.0.0", 134 | "electron-updater": "^4.1.2", 135 | "ini": "^1.3.5", 136 | "lodash": "^4.17.14", 137 | "prop-types": "^15.7.2", 138 | "react": "^16.8.6", 139 | "react-dom": "^16.8.6", 140 | "react-redux": "^7.1.0", 141 | "react-router-dom": "^5.0.1", 142 | "redux": "^4.0.4", 143 | "styled-components": "^4.3.2" 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | module.exports = { 4 | printWidth: 100, 5 | tabWidth: 2, // Default 6 | useTabs: false, // Default 7 | semi: false, 8 | singleQuote: true, 9 | quoteProps: 'as-needed', // Default 10 | jsxSingleQuote: false, // Default 11 | trailingComma: 'none', // Default 12 | bracketSpacing: true, // Default 13 | jsxBracketSameLine: false, // Default 14 | arrowParens: 'avoid', // Default 15 | requirePragma: false, // Default 16 | insertPragma: true, 17 | proseWrap: 'preserve', // Default 18 | htmlWhitespaceSensitivity: 'css', // Default 19 | endOfLine: 'auto' 20 | } 21 | -------------------------------------------------------------------------------- /resources/activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/activity.png -------------------------------------------------------------------------------- /resources/build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icon.icns -------------------------------------------------------------------------------- /resources/build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icon.ico -------------------------------------------------------------------------------- /resources/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icon.png -------------------------------------------------------------------------------- /resources/build/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/128x128.png -------------------------------------------------------------------------------- /resources/build/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/16x16.png -------------------------------------------------------------------------------- /resources/build/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/24x24.png -------------------------------------------------------------------------------- /resources/build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/256x256.png -------------------------------------------------------------------------------- /resources/build/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/32x32.png -------------------------------------------------------------------------------- /resources/build/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/48x48.png -------------------------------------------------------------------------------- /resources/build/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/512x512.png -------------------------------------------------------------------------------- /resources/build/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/64x64.png -------------------------------------------------------------------------------- /resources/build/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/build/icons/96x96.png -------------------------------------------------------------------------------- /resources/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/dashboard.png -------------------------------------------------------------------------------- /resources/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/settings.png -------------------------------------------------------------------------------- /resources/virgo-full.logoist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/virgo-full.logoist -------------------------------------------------------------------------------- /resources/virgo.logoist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/resources/virgo.logoist -------------------------------------------------------------------------------- /scripts/install_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | sudo apt-get -qq update 7 | sudo apt-get -qq install -y -qq apt-transport-https ca-certificates curl software-properties-common 8 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 9 | sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /dev/null 2>&1 10 | sudo apt-get -qq update 11 | sudo apt-get -qq install -y -qq docker-ce 12 | sudo usermod -aG docker $USER 13 | 14 | echo $'{\n "experimental": true\n}' | sudo tee -a /etc/docker/daemon.json 15 | mkdir -p ~/.docker && echo $'{\n "experimental": enabled\n}' | sudo tee -a ~/.docker/daemon.json 16 | 17 | sudo systemctl daemon-reload 18 | sudo systemctl restart docker 19 | 20 | echo "Please log out and back in." 21 | 22 | -------------------------------------------------------------------------------- /src/main/__tests__/app.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Application } from 'spectron' 4 | import { resolve } from 'app-root-path' 5 | 6 | export default () => 7 | new Application({ 8 | path: resolve(`./node_modules/.bin/electron${process.platform === 'win32' ? '.cmd' : ''}`), 9 | args: [resolve('.')], 10 | env: { 11 | NODE_ENV: 'test' 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /src/main/__tests__/main.test.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import Application from './app' 4 | 5 | let app 6 | 7 | beforeAll(async () => { 8 | app = Application() 9 | await app.start() 10 | await app.client.waitUntilWindowLoaded() 11 | }) 12 | 13 | describe('App', () => { 14 | test('Application Title', async () => { 15 | expect(await app.client.getTitle()).toBe('Virgo') 16 | }) 17 | }) 18 | 19 | afterAll(async () => { 20 | if (app && app.isRunning()) { 21 | await app.stop() 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/main/addons.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import installExtension, { 4 | REACT_DEVELOPER_TOOLS, 5 | REDUX_DEVTOOLS, 6 | REACT_PERF 7 | } from 'electron-devtools-installer' 8 | import Logger from '../shared/logger' 9 | 10 | const logger = new Logger('Addons') 11 | 12 | const toolMap = { 13 | REACT_DEVELOPER_TOOLS, 14 | REDUX_DEVTOOLS, 15 | REACT_PERF 16 | } 17 | 18 | // eslint-disable-next-line import/prefer-default-export 19 | export const installDeveloperTools = tools => { 20 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS 21 | tools.map(async toolName => { 22 | try { 23 | const name = await installExtension(toolMap[toolName], forceDownload) 24 | logger.info(`Added extension: ${name}`) 25 | } catch (error) { 26 | logger.info(`An error occurred: ${error}`) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/main/api/docker.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import fs from 'fs' 4 | import stream from 'stream' 5 | 6 | import Docker from 'dockerode' 7 | 8 | import Logger from '../../shared/logger' 9 | 10 | const logger = new Logger('DockerManager') 11 | 12 | class DockerManager { 13 | constructor(userOptions = {}) { 14 | let status 15 | let options = {} 16 | 17 | logger.info(`Initializing DockerManager for Platform: ${process.platform}`) 18 | 19 | if (process.platform === 'win32') { 20 | options = { 21 | socketPath: '//./pipe/docker_engine' 22 | } 23 | } else { 24 | options = { 25 | socketPath: process.env.DOCKER_SOCKET || '/var/run/docker.sock' 26 | } 27 | } 28 | options = Object.assign({}, options, userOptions) 29 | 30 | try { 31 | if (process.platform === 'win32') { 32 | status = fs.statSync(options.socketPath).isFile() 33 | } else { 34 | status = fs.statSync(options.socketPath).isSocket() 35 | } 36 | } catch (error) { 37 | status = false 38 | } 39 | 40 | if (!status) { 41 | throw new Error('Are you sure Docker is running?') 42 | } 43 | 44 | this.docker = new Docker(options) 45 | } 46 | 47 | async info() { 48 | return this.docker.info() 49 | } 50 | 51 | async getContainer(id) { 52 | return this.docker.getContainer(id) 53 | } 54 | 55 | pull(name, options) { 56 | return new Promise((resolve, reject) => { 57 | this.docker.pull(name, options, (error, stream) => { 58 | if (error) { 59 | reject(error) 60 | return 61 | } 62 | 63 | // eslint-disable-next-line no-shadow,no-unused-vars 64 | const onFinished = (error, output) => { 65 | if (error) { 66 | reject(error) 67 | return 68 | } 69 | logger.info(`Successfully pulled ${name}.`) 70 | resolve() 71 | } 72 | 73 | // eslint-disable-next-line no-unused-vars 74 | const onProgress = event => {} 75 | 76 | this.docker.modem.followProgress(stream, onFinished, onProgress) 77 | }) 78 | }) 79 | } 80 | 81 | async runCommand(container, command, timeout = -1) { 82 | const options = { 83 | Cmd: ['bash', '-c', command], 84 | AttachStdout: true, 85 | AttachStderr: true 86 | } 87 | return container.exec(options).then(exec => { 88 | return new Promise((resolve, reject) => { 89 | exec.start((error, containerStream) => { 90 | if (error) { 91 | reject(error) 92 | } 93 | let data = '' 94 | const outStream = new stream.PassThrough() 95 | outStream.on('data', chunk => { 96 | data += chunk.toString('utf-8') 97 | }) 98 | containerStream.on('end', () => { 99 | outStream.end() 100 | resolve(data) 101 | }) 102 | container.modem.demuxStream(containerStream, outStream, outStream) 103 | if (timeout > 0) { 104 | setTimeout(() => containerStream.destroy(), timeout) 105 | } 106 | }) 107 | }) 108 | }) 109 | } 110 | } 111 | 112 | export default DockerManager 113 | -------------------------------------------------------------------------------- /src/main/api/ipc.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ipcMain } from 'electron' 4 | 5 | import DockerManager from './docker' 6 | import Logger from '../../shared/logger' 7 | import Serializers from '../../shared/serializers' 8 | 9 | const logger = new Logger('IPC') 10 | 11 | // try { 12 | const dockerManager = new DockerManager() 13 | // } catch (error) { 14 | // Error: ENOENT: no such file or directory, stat '/var/run/docker.sock' 15 | // Call setup routines for Docker engine. 16 | // console.log(error) 17 | // } 18 | 19 | const getErrorMessage = error => { 20 | /* 21 | Variation #1 22 | error -> {"statusCode":502,"json":"Bad response from Docker engine\n"} 23 | error.message -> "(HTTP code 502) unexpected - Bad response from Docker engine\n " 24 | 25 | Variation #2 26 | error -> {"reason":"no such container","statusCode":404,"json":{"message":"..."}} 27 | 28 | Variation #3 29 | error -> {"statusCode":502,"json":null} 30 | */ 31 | if (Object.prototype.hasOwnProperty.call(error, 'json')) { 32 | switch (typeof error.json) { 33 | case 'string': 34 | return error.json 35 | case 'object': 36 | if (error.json !== null && 'message' in error.json) { 37 | return error.json.message 38 | } 39 | break 40 | default: 41 | return 'Unknown error message.' 42 | } 43 | } 44 | if (Object.prototype.hasOwnProperty.call(error, 'message')) { 45 | return error.message 46 | } 47 | if (typeof error === 'string') { 48 | return error 49 | } 50 | return 'Unknown error type.' 51 | } 52 | 53 | const getError = error => { 54 | return { error: true, code: error.statusCode, message: getErrorMessage(error) } 55 | } 56 | 57 | /** 58 | * Pull an image, create a container and start that container. 59 | */ 60 | ipcMain.on('container.run', (event, args) => { 61 | const { 62 | task: { name, environment }, 63 | volumes, 64 | pullOptions 65 | } = args 66 | 67 | const createOptions = { 68 | Image: name, 69 | Tty: true, 70 | Env: environment || [], 71 | Volumes: { 72 | '/home/worker': {} 73 | }, 74 | HostConfig: { 75 | AutoRemove: true, 76 | Binds: [...volumes] 77 | } 78 | } 79 | 80 | logger.info(`Task environment: ${environment}`) 81 | 82 | dockerManager 83 | .pull(name, pullOptions || {}) // || { platform: 'linux' } 84 | .then(() => { 85 | event.sender.send('image.pull', name) 86 | /** 87 | * Create a container of the pulled image. Each container removes itself on error or on stop. 88 | */ 89 | dockerManager.docker 90 | .createContainer(createOptions) 91 | .then(container => { 92 | logger.info('Launching container...') 93 | return container.start() 94 | }) 95 | .then(container => { 96 | logger.info(`Container ${container.id} started!`) 97 | event.sender.send('container.run', container) 98 | }) 99 | .catch(error => { 100 | logger.error(`Container start error: ${JSON.stringify(error)}`) 101 | event.sender.send('container.error', getError(error)) 102 | }) 103 | }) 104 | .catch(error => { 105 | logger.error(`Image pull error: ${JSON.stringify(error)}`) 106 | event.sender.send('image.error', getError(error)) 107 | }) 108 | }) 109 | 110 | /* 111 | * Stop a running container. 112 | */ 113 | ipcMain.on('container.stop', (event, args) => { 114 | const { id } = args 115 | 116 | dockerManager 117 | .getContainer(id) 118 | .then(container => { 119 | return container.stop() 120 | }) 121 | .then(() => { 122 | event.sender.send('container.stop', id) 123 | }) 124 | .catch(error => { 125 | logger.error(`Container stop error: ${JSON.stringify(error)}`) 126 | event.sender.send('container.error', getError(error)) 127 | }) 128 | }) 129 | 130 | /** 131 | * Pause a running container. 132 | */ 133 | ipcMain.on('container.pause', (event, args) => { 134 | const { id } = args 135 | 136 | dockerManager 137 | .getContainer(id) 138 | .then(container => { 139 | return container.pause() 140 | }) 141 | .then(() => { 142 | event.sender.send('container.pause', id) 143 | }) 144 | .catch(error => { 145 | logger.error(`Container pause error: ${JSON.stringify(error)}`) 146 | event.sender.send('container.error', getError(error)) 147 | }) 148 | }) 149 | 150 | /** 151 | * Unpause a paused container. 152 | */ 153 | ipcMain.on('container.unpause', (event, args) => { 154 | const { id } = args 155 | 156 | dockerManager 157 | .getContainer(id) 158 | .then(container => { 159 | return container.unpause() 160 | }) 161 | .then(() => { 162 | event.sender.send('container.unpause', id) 163 | }) 164 | .catch(error => { 165 | logger.error(`Container unpause error: ${JSON.stringify(error)}`) 166 | event.sender.send('container.error', getError(error)) 167 | }) 168 | }) 169 | 170 | /** 171 | * Remove a container. 172 | */ 173 | ipcMain.on('container.remove', (event, args) => { 174 | const { id } = args 175 | 176 | dockerManager 177 | .getContainer(id) 178 | .then(container => { 179 | return container.remove({ force: true }) 180 | }) 181 | .then(() => { 182 | event.sender.send('container.remove', { data: null, error: false }) 183 | }) 184 | .catch(error => { 185 | logger.error(`Container remove error: ${JSON.stringify(error)}`) 186 | event.sender.send('container.remove', getError(error)) 187 | }) 188 | }) 189 | 190 | /** 191 | * List stopped and started containers. 192 | */ 193 | // eslint-disable-next-line no-unused-vars 194 | ipcMain.on('container.list', (event, args) => { 195 | dockerManager.docker.listContainers({ all: true }).then(containers => { 196 | event.sender.send('container.list', containers) 197 | }) 198 | }) 199 | 200 | /** 201 | * List downloaded images. 202 | */ 203 | // eslint-disable-next-line no-unused-vars 204 | ipcMain.on('image.list', (event, args) => { 205 | dockerManager.docker.listImages().then(images => { 206 | event.sender.send('image.list', images) 207 | }) 208 | }) 209 | 210 | /** 211 | * Remove downloaded image. 212 | */ 213 | ipcMain.on('image.remove', (event, args) => { 214 | const { name } = args 215 | 216 | dockerManager.docker 217 | .getImage(name) 218 | .remove() 219 | .then(image => { 220 | event.sender.send('image.remove', { data: image, error: false }) 221 | }) 222 | .catch(error => { 223 | logger.error(`Image remove error: ${JSON.stringify(error)}`) 224 | event.sender.send('image.remove', getError(error)) 225 | }) 226 | }) 227 | 228 | /** 229 | * Get container information. 230 | */ 231 | ipcMain.on('container.inspect', (event, args) => { 232 | const { id } = args 233 | 234 | dockerManager 235 | .getContainer(id) 236 | .then(container => { 237 | return container.inspect() 238 | }) 239 | .then(data => { 240 | event.sender.send('container.inspect', { data, error: false }) 241 | }) 242 | .catch(error => { 243 | logger.error(`Container inspect error: ${JSON.stringify(error)}`) 244 | event.sender.send('container.inspect', getError(error)) 245 | }) 246 | }) 247 | 248 | ipcMain.on('container.stats', async (event, args) => { 249 | const { serializerName, customCommand, id } = args 250 | 251 | let result = { 252 | data: null, 253 | error: false 254 | } 255 | 256 | if (Object.keys(Serializers).includes(serializerName)) { 257 | try { 258 | const serializer = Serializers[serializerName] 259 | const container = await dockerManager.getContainer(id) 260 | const commandOutput = await dockerManager.runCommand(container, customCommand || serializer.command) 261 | result.data = serializer.serialize(commandOutput) 262 | } catch (error) { 263 | result.error = true 264 | logger.error(error) 265 | } 266 | } 267 | 268 | logger.debug(`Serialized command output: ${JSON.stringify(result)}`) 269 | event.sender.send('container.stats', result) 270 | }) 271 | -------------------------------------------------------------------------------- /src/main/main.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { app, dialog } from 'electron' 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | import Sentry from '../shared/sentry' 7 | import { setupUpdater } from './updater' 8 | import { Package, Environment } from '../shared/common' 9 | import Logger from '../shared/logger' 10 | import createTray from './tray' 11 | import createMainWindow from './windows/main' 12 | import './api/ipc' 13 | 14 | const logger = new Logger('MainProcess') 15 | 16 | if (Environment.isDevelopment) { 17 | /* Disable annoying security warnings because we run a development server. */ 18 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true 19 | } 20 | 21 | // eslint-disable-next-line no-unused-vars 22 | let tray 23 | let mainWindow 24 | 25 | const instanceLock = app.requestSingleInstanceLock() 26 | if (!instanceLock) { 27 | app.quit() 28 | } 29 | 30 | const createWindow = () => { 31 | mainWindow = createMainWindow() 32 | tray = createTray(mainWindow) 33 | 34 | if (!Environment.isWindows) { 35 | app.setAboutPanelOptions({ 36 | applicationName: Package.name, 37 | applicationVersion: Package.version 38 | }) 39 | } 40 | } 41 | 42 | // eslint-disable-next-line no-unused-vars 43 | app.on('second-instance', (event, argv, cwd) => { 44 | if (mainWindow) { 45 | if (mainWindow.isMinimized()) { 46 | mainWindow.restore() 47 | } 48 | mainWindow.focus() 49 | } 50 | }) 51 | 52 | app.on('ready', () => { 53 | createWindow() 54 | setupUpdater(mainWindow) 55 | }) 56 | 57 | app.on('window-all-closed', () => { 58 | if (!Environment.isMacOS) { 59 | app.quit() 60 | } 61 | }) 62 | 63 | app.on('before-quit', () => { 64 | if (Environment.isMacOS) { 65 | app.quitting = true 66 | } 67 | }) 68 | 69 | app.on('activate', () => { 70 | if (Environment.isMacOS && mainWindow !== null) { 71 | mainWindow.show() 72 | } 73 | }) 74 | 75 | process.on('uncaughtException', error => { 76 | const message = 'Uncaught exception in the Main process. Application will shut down.' 77 | const messageBoxOptions = { 78 | type: 'error', 79 | title: 'Main process crashed unexpectedly!', 80 | message 81 | } 82 | logger.error(message) 83 | dialog.showMessageBox(messageBoxOptions) 84 | }) 85 | -------------------------------------------------------------------------------- /src/main/store.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import Store from 'electron-store' 4 | import { defaultsDeep } from 'lodash' 5 | 6 | import { Environment, JS } from '../shared/common' 7 | import Logger from '../shared/logger' 8 | 9 | const logger = new Logger('Preferences') 10 | 11 | const defaults = { 12 | preferences: { 13 | darkMode: true, 14 | vibrance: false, 15 | restoreWindowSize: true, 16 | alwaysOnTop: false, 17 | winBounds: { 18 | x: null, 19 | y: null, 20 | width: 940, 21 | height: 780 22 | }, 23 | taskURL: 'https://virgo-tasks.herokuapp.com/tasks', 24 | contactEmail: 'fuzzing@mozilla.com', 25 | earlyReleases: false, 26 | backend: { 27 | fuzzmanager: { 28 | serverhost: 'virgo-f.fuzzing.mozilla.org', 29 | serverport: 443, 30 | serverproto: 'https', 31 | serverauthtoken: '13374cc355', 32 | clientid: '' 33 | } 34 | }, 35 | sentry: { 36 | dsn: 'https://165a946fbc3445aea4091b5e704e6a09@sentry.io/1459621' 37 | } 38 | } 39 | } 40 | 41 | /* 42 | A new Virgo release might deprecate or introduce new settings. 43 | * If we deprecate settings, then we need to detect those obsolete settings and remove them from 44 | the user's settings. 45 | * If we introduce settings, then we need to merge those new settings but keep the user's set values 46 | for existing and non obsolete settings. 47 | */ 48 | 49 | const store = new Store({ defaults }) 50 | 51 | const migratePrefs = () => { 52 | const obsoletePrefs = JS.compareJSON(store.store.preferences, defaults.preferences) 53 | if (obsoletePrefs.length > 0) { 54 | logger.warning( 55 | 'The following prefs are removed from your preferences because they are obsolete:' 56 | ) 57 | logger.warning(JSON.stringify(obsoletePrefs)) 58 | try { 59 | obsoletePrefs.forEach(pref => { 60 | logger.debug(`preferences.${pref}`) 61 | store.delete(`preferences.${pref}`) 62 | }) 63 | } catch (error) { 64 | logger.error(error) 65 | process.exit(1) 66 | } 67 | } 68 | 69 | logger.warning('We now migrate newly added preferences to your existing preferences.') 70 | store.set(defaultsDeep(store.store, defaults)) 71 | 72 | logger.info('Update done.') 73 | } 74 | 75 | if (Environment.isMainProcess) { 76 | migratePrefs() 77 | } 78 | 79 | export default store 80 | -------------------------------------------------------------------------------- /src/main/tray.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Tray, Menu } from 'electron' 4 | import { resolve } from 'app-root-path' 5 | 6 | import { Environment, JS } from '../shared/common' 7 | 8 | export default function createTray(window) { 9 | let template = [ 10 | { 11 | label: 'Settings' 12 | }, 13 | { 14 | label: 'Toggle Window', 15 | click: () => { 16 | // eslint-disable-next-line no-unused-expressions 17 | window.isVisible() ? window.hide() : window.show() 18 | } 19 | }, 20 | { 21 | label: 'Quit', 22 | accelerator: Environment.isMacOS ? 'Command+Q' : 'Ctrl+Q', 23 | selector: 'terminate:' 24 | } 25 | ] 26 | 27 | if (Environment.isDevelopment) { 28 | template = JS.insert(template, 1, { 29 | label: 'Toggle DevTools', 30 | accelerator: Environment.isMacOS ? 'Option+Command+I' : 'Alt+Command+I', 31 | click: () => { 32 | window.show() 33 | window.toggleDevTools() 34 | } 35 | }) 36 | } 37 | 38 | const tray = new Tray(resolve('resources/build/icons/16x16.png')) 39 | tray.setToolTip('Virgo') 40 | tray.setContextMenu(Menu.buildFromTemplate(template)) 41 | 42 | return tray 43 | } 44 | -------------------------------------------------------------------------------- /src/main/updater.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { app, dialog, BrowserWindow, ipcMain } from 'electron' 4 | import { autoUpdater } from 'electron-updater' 5 | import appRoot from 'app-root-path' 6 | 7 | import Logger from '../shared/logger' 8 | import Store from './store' 9 | import { Environment } from '../shared/common' 10 | 11 | const logger = new Logger('Updater') 12 | 13 | autoUpdater.logger = logger 14 | autoUpdater.allowPrerelease = Store.get('allowPreRelease', false) 15 | 16 | if (Environment.isDevelopment) { 17 | autoUpdater.updateConfigPath = appRoot.resolve('./configs/minio/dev-app-update.yml') 18 | } 19 | 20 | const ensureSafeQuitAndInstall = () => { 21 | /* Close windows more aggressively because of the custom MacOS window close functionality. */ 22 | const isSilent = true 23 | const isForceRunAfter = true 24 | app.removeAllListeners('window-all-closed') 25 | BrowserWindow.getAllWindows().map(browserWindow => browserWindow.removeAllListeners('close')) 26 | autoUpdater.quitAndInstall(isSilent, isForceRunAfter) 27 | } 28 | 29 | export const isNetworkError = error => { 30 | return ( 31 | error.message === 'net::ERR_CONNECTION_REFUSED' || 32 | error.message === 'net::ERR_INTERNET_DISCONNECTED' || 33 | error.message === 'net::ERR_PROXY_CONNECTION_FAILED' || 34 | error.message === 'net::ERR_CONNECTION_RESET' || 35 | error.message === 'net::ERR_CONNECTION_CLOSE' || 36 | error.message === 'net::ERR_NAME_NOT_RESOLVED' || 37 | error.message === 'net::ERR_CONNECTION_TIMED_OUT' 38 | ) 39 | } 40 | 41 | export const checkUpdates = () => { 42 | autoUpdater.checkForUpdates().catch(error => { 43 | if (isNetworkError(error)) { 44 | logger.error('Network error.') 45 | } else { 46 | logger.error(`Error: ${error === null ? 'Unknown' : (error.message || error).toString()}`) 47 | } 48 | }) 49 | } 50 | 51 | export const setupUpdater = window => { 52 | const dispatch = data => { 53 | logger.info(`Dispatch: ${JSON.stringify(data)}`) 54 | window.webContents.send('updateMessage', data) 55 | } 56 | 57 | autoUpdater.on('error', error => { 58 | dispatch({ msg: `😱 ${error}` }) 59 | }) 60 | 61 | autoUpdater.on('checking-for-update', () => { 62 | dispatch({ msg: `🔎 Checking for updates ...` }) 63 | }) 64 | 65 | // eslint-disable-next-line no-unused-vars 66 | autoUpdater.on('update-available', info => { 67 | dispatch({ msg: `🎉 Update available. Downloading ...`, hide: false }) 68 | }) 69 | 70 | // eslint-disable-next-line no-unused-vars 71 | autoUpdater.on('update-not-available', info => { 72 | dispatch({ msg: '😊 You are using the latest version.' }) 73 | }) 74 | 75 | autoUpdater.on('download-progress', progress => { 76 | window.webContents.send('download-progress', { 77 | percent: progress.percent, 78 | speed: progress.bytesPerSecond 79 | }) 80 | }) 81 | 82 | // eslint-disable-next-line no-unused-vars 83 | autoUpdater.on('update-downloaded', info => { 84 | dispatch({ msg: `🤘 Update downloaded.` }) 85 | 86 | const messageBoxOptions = { 87 | type: 'question', 88 | title: 'Found Updates', 89 | message: `A new version has been downloaded. Would you like to install it now?`, 90 | buttons: ['Yes', 'No'], 91 | defaultId: 0 92 | } 93 | 94 | dialog.showMessageBox(messageBoxOptions, response => { 95 | if (response === 0) { 96 | ensureSafeQuitAndInstall() 97 | } 98 | }) 99 | }) 100 | 101 | dispatch({ msg: `🖥 App version: ${app.getVersion()}` }) 102 | 103 | if (!Environment.isDevelopment) { 104 | checkUpdates() 105 | } 106 | } 107 | 108 | ipcMain.on('updateCheck', () => { 109 | checkUpdates() 110 | }) 111 | 112 | export default autoUpdater 113 | -------------------------------------------------------------------------------- /src/main/windows/main.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import * as url from 'url' 4 | import { BrowserWindow, app } from 'electron' 5 | import { resolve } from 'app-root-path' 6 | 7 | import { Environment } from '../../shared/common' 8 | import Store from '../store' 9 | import Logger from '../../shared/logger' 10 | 11 | const logger = new Logger('Window:Main') 12 | 13 | export default function createMainWindow() { 14 | let options = { 15 | autoHideMenuBar: true, 16 | frame: false, 17 | minWidth: 940, 18 | minHeight: 780, 19 | show: false, 20 | alwaysOnTop: Store.get('preferences.alwaysOnTop'), 21 | titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default', 22 | webPreferences: { 23 | nodeIntegration: true 24 | } 25 | } 26 | 27 | // Restore window size and position. 28 | if (Store.get('preferences.restoreWindowSize') === true) { 29 | options = Object.assign(options, Store.get('preferences.winBounds')) 30 | } 31 | 32 | let window = new BrowserWindow(options) 33 | 34 | const appUrl = 35 | Environment.isPackaged || Environment.isTest 36 | ? url.format({ 37 | pathname: resolve('build/app/renderer/production/index.html'), 38 | protocol: 'file:', 39 | slashes: true 40 | }) 41 | : Environment.developmentURL 42 | 43 | window.loadURL(appUrl) 44 | 45 | if (Environment.isDevelopment) { 46 | ;(async () => { 47 | const addons = await import('../addons') 48 | addons.installDeveloperTools(['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']) 49 | })() 50 | } 51 | 52 | /* 53 | * This event is usually emitted after the did-finish-load event, but for pages with many 54 | * remote resources, it may be emitted before the did-finish-load event. 55 | * window.once('ready-to-show', () => {}) 56 | */ 57 | window.webContents.on('did-finish-load', () => { 58 | if (!window) { 59 | throw new Error('window is not defined.') 60 | } 61 | if (process.env.START_MINIMIZED) { 62 | window.minimize() 63 | } else { 64 | window.show() 65 | window.focus() 66 | } 67 | }) 68 | 69 | window.on('close', event => { 70 | // Save window size and position. 71 | Store.set('preferences.winBounds', window.getBounds()) 72 | 73 | if (Environment.isMacOS) { 74 | if (app.quitting) { 75 | /* User tried to quit the app for real. */ 76 | window = null 77 | } else if (window !== null) { 78 | /* User tried to close the window and we hide the main Window instead. */ 79 | logger.info('Simulating App close on MacOS.') 80 | event.preventDefault() 81 | window.hide() 82 | } 83 | } 84 | }) 85 | 86 | window.on('closed', () => { 87 | logger.info('App closed.') 88 | window = null 89 | }) 90 | 91 | window.on('unresponsive', () => { 92 | logger.warn('App became unresponsive.') 93 | }) 94 | 95 | window.webContents.on('crashed', () => { 96 | logger.error(`WebContents crashed.`) 97 | }) 98 | 99 | return window 100 | } 101 | -------------------------------------------------------------------------------- /src/renderer/components/CrashPage.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | import { Link } from 'react-router-dom' 6 | 7 | import { Grid } from '@material-ui/core' 8 | import Button from '@material-ui/core/Button' 9 | import Typography from '@material-ui/core/Typography' 10 | import { withStyles } from '@material-ui/core/styles' 11 | 12 | import Logger from '../../shared/logger' 13 | 14 | const logger = new Logger('CrashPage') 15 | 16 | const styles = theme => ({ 17 | root: { 18 | backgroundColor: '#2b2c38', 19 | padding: '10%', 20 | height: '100%', 21 | margin: 0 22 | }, 23 | grid: { 24 | paddingTop: '100px' 25 | }, 26 | text: { 27 | color: '#ffffff' 28 | } 29 | }) 30 | 31 | const Preferences = props => 32 | const Dashboard = props => 33 | 34 | class CrashPage extends React.Component { 35 | render() { 36 | const { classes } = this.props 37 | 38 | return ( 39 |
40 | 41 | Crashed :-/ 42 | 43 | 44 | The error has been sent to us. 45 | 46 | 54 | 55 | 58 | 59 | 60 | 63 | 64 | 65 |
66 | ) 67 | } 68 | } 69 | 70 | CrashPage.propTypes = { 71 | classes: PropTypes.object.isRequired 72 | } 73 | 74 | export default withStyles(styles, { withTheme: true })(CrashPage) 75 | -------------------------------------------------------------------------------- /src/renderer/components/DarkmodeSwitch.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | /* Styles */ 7 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 8 | import ListItemIcon from '@material-ui/core/ListItemIcon' 9 | import DarkIcon from '@material-ui/icons/Brightness2' 10 | import LightIcon from '@material-ui/icons/BrightnessLow' 11 | import ListItemText from '@material-ui/core/ListItemText' 12 | import Switch from '@material-ui/core/Switch' 13 | import { withStyles } from '@material-ui/core/styles' 14 | 15 | // eslint-disable-next-line no-unused-vars 16 | const styles = theme => ({}) 17 | 18 | const DarkmodeSwitch = props => { 19 | const { label1, label2, checked, onChange, helpText } = props 20 | return ( 21 | 22 | {checked ? : } 23 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | DarkmodeSwitch.defaultProps = { 37 | label1: 'Light Mode', 38 | label2: 'Dark Mode' 39 | } 40 | 41 | DarkmodeSwitch.propTypes = { 42 | label1: PropTypes.string, 43 | label2: PropTypes.string, 44 | checked: PropTypes.bool.isRequired, 45 | onChange: PropTypes.func.isRequired 46 | } 47 | 48 | export default withStyles(styles)(DarkmodeSwitch) 49 | -------------------------------------------------------------------------------- /src/renderer/components/Error/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | import * as Sentry from '@sentry/electron' 6 | import { withRouter } from 'react-router-dom' 7 | import { withStyles } from '@material-ui/core/styles' 8 | 9 | import Logger from '../../../shared/logger' 10 | import { Environment } from '../../../shared/common' 11 | import ErrorDialog from './ErrorDialog' 12 | import CrashPage from '../CrashPage' 13 | 14 | const logger = new Logger('ErrorBoundary') 15 | 16 | const styles = theme => ({ 17 | summary: { 18 | outline: 'none !important' 19 | } 20 | }) 21 | 22 | class ErrorBoundary extends React.Component { 23 | state = { error: null, errorInfo: null, eventId: null } 24 | 25 | sendReport = () => { 26 | const { error, errorInfo, eventId } = this.state 27 | if (!Environment.isTest) { 28 | /* Send the crash report to Sentry. */ 29 | Sentry.withScope(scope => { 30 | scope.setExtra(errorInfo) 31 | this.setState({ eventId: Sentry.captureException(error) }) 32 | }) 33 | /* Collect user feedback as follow up. */ 34 | Sentry.showReportDialog({ eventId }) 35 | } else { 36 | logger.error(`Error: ${error}`) 37 | logger.error(`ErrorInfo: ${JSON.stringify(errorInfo)}`) 38 | } 39 | } 40 | 41 | errorContent = (error, errorInfo) => { 42 | const { classes } = this.props 43 | 44 | return ( 45 |
46 | See Details 47 | {error && error.toString()} 48 |
49 |
{errorInfo.componentStack}
50 |
51 | ) 52 | } 53 | 54 | componentDidCatch(error, errorInfo) { 55 | const { history } = this.props 56 | 57 | this.setState({ error, errorInfo }) 58 | 59 | history.listen((location, action) => { 60 | if (this.state.error) { 61 | this.setState({ error: null }) 62 | } 63 | }) 64 | } 65 | 66 | render() { 67 | const { children } = this.props 68 | const { error, errorInfo } = this.state 69 | 70 | if (error) { 71 | return ( 72 |
73 | 74 | this.sendReport()} 78 | /> 79 |
80 | ) 81 | } 82 | 83 | return children 84 | } 85 | } 86 | 87 | ErrorBoundary.propTypes = { 88 | children: PropTypes.node.isRequired, 89 | classes: PropTypes.object.isRequired 90 | } 91 | 92 | export default withRouter(withStyles(styles, { withTheme: true })(ErrorBoundary)) 93 | -------------------------------------------------------------------------------- /src/renderer/components/Error/ErrorDialog.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | import Button from '@material-ui/core/Button' 7 | import Dialog from '@material-ui/core/Dialog' 8 | import DialogActions from '@material-ui/core/DialogActions' 9 | import DialogContent from '@material-ui/core/DialogContent' 10 | import DialogTitle from '@material-ui/core/DialogTitle' 11 | import { Typography, withStyles } from '@material-ui/core' 12 | 13 | const errorTitles = [ 14 | 'Abort mission, back to base.', 15 | 'We have a problem ...', 16 | 'Stay calm and do not panic!' 17 | ] 18 | 19 | const styles = theme => ({}) 20 | 21 | export const randomErrorTitle = () => { 22 | return errorTitles[Math.floor(Math.random() * errorTitles.length)] 23 | } 24 | 25 | class ErrorDialog extends React.Component { 26 | static defaultProps = { 27 | title: randomErrorTitle() 28 | } 29 | 30 | state = { 31 | open: true 32 | } 33 | 34 | onSuccess = () => { 35 | const { onSuccessCallback } = this.props 36 | 37 | onSuccessCallback() 38 | this.setState({ open: false }) 39 | } 40 | 41 | onClose = () => { 42 | this.setState({ open: false }) 43 | } 44 | 45 | onAbort = () => { 46 | this.setState({ open: false }) 47 | } 48 | 49 | render() { 50 | const { open } = this.state 51 | const { title, content } = this.props 52 | 53 | return ( 54 |
55 | 63 | {title} 64 | 65 | {content} 66 | 67 | 68 | 71 | 74 | 75 | 76 |
77 | ) 78 | } 79 | } 80 | 81 | ErrorDialog.propTypes = { 82 | onSuccessCallback: PropTypes.func.isRequired, 83 | content: PropTypes.node.isRequired, 84 | title: PropTypes.string 85 | } 86 | 87 | export default withStyles(styles, { withTheme: true })(ErrorDialog) 88 | -------------------------------------------------------------------------------- /src/renderer/components/LogoIcon.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | /* Styles */ 7 | import IconButton from '@material-ui/core/IconButton' 8 | import { withStyles } from '@material-ui/core/styles' 9 | 10 | const styles = theme => ({ 11 | hide: { 12 | display: 'none' 13 | }, 14 | root: { 15 | padding: '8px', 16 | ...theme.mixins.IconButton 17 | } 18 | }) 19 | 20 | const LogoIcon = props => { 21 | const { classes, hide, children, ...other } = props 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ) 28 | } 29 | 30 | LogoIcon.propTypes = { 31 | classes: PropTypes.object.isRequired, 32 | hide: PropTypes.bool.isRequired, 33 | children: PropTypes.node.isRequired 34 | } 35 | 36 | export default withStyles(styles)(LogoIcon) 37 | -------------------------------------------------------------------------------- /src/renderer/components/TimeCounter.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | /* Styles */ 7 | import { withStyles } from '@material-ui/core/styles' 8 | 9 | // eslint-disable-next-line no-unused-vars 10 | const styles = theme => ({}) 11 | 12 | class TimeCounter extends React.Component { 13 | getUnits = timeElapsed => { 14 | const seconds = timeElapsed / 1000 15 | const hh = Math.floor(seconds / 3600).toString() 16 | const mm = Math.floor((seconds - hh * 3600) / 60).toString() 17 | const ss = Math.floor(seconds - hh * 3600 - mm * 60).toString() 18 | return { hh, mm, ss } 19 | } 20 | 21 | leftPad = (n, width = 2) => { 22 | if (n.length > width) { 23 | return n 24 | } 25 | const padding = new Array(width).join('0') 26 | return (padding + n).slice(-width) 27 | } 28 | 29 | render() { 30 | const { timeElapsed } = this.props 31 | const units = this.getUnits(timeElapsed) 32 | return ( 33 |
34 | {this.leftPad(units.hh)}:{this.leftPad(units.mm)}:{this.leftPad(units.ss)} 35 |
36 | ) 37 | } 38 | } 39 | 40 | /* PropTypes */ 41 | TimeCounter.propTypes = { 42 | classes: PropTypes.object.isRequired, 43 | timeElapsed: PropTypes.number.isRequired 44 | } 45 | 46 | export default withStyles(styles)(TimeCounter) 47 | -------------------------------------------------------------------------------- /src/renderer/containers/Activity/DockerContainers.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | /* eslint-disable no-underscore-dangle */ 3 | 4 | import React from 'react' 5 | import PropTypes from 'prop-types' 6 | import { ipcRenderer } from 'electron' 7 | 8 | /* Styles */ 9 | import { 10 | Table, 11 | TableBody, 12 | TableCell, 13 | TablePagination, 14 | TableRow, 15 | Paper, 16 | Checkbox 17 | } from '@material-ui/core' 18 | import { withStyles } from '@material-ui/core/styles' 19 | 20 | import EnhancedTableHead from './Table/TableHead' 21 | import EnhancedTableToolbar from './Table/TableToolbar' 22 | import { stableSort, getSorting } from './Table/helpers' 23 | 24 | import Logger from '../../../shared/logger' 25 | import { mapContainers } from '../../../shared/docker' 26 | 27 | const logger = new Logger('DockerContainers') 28 | 29 | // eslint-disable-next-line no-unused-vars 30 | const styles = theme => ({ 31 | root: { 32 | width: '100%' 33 | }, 34 | table: {}, 35 | tableWrapper: { 36 | overflowX: 'auto' 37 | } 38 | }) 39 | 40 | const rows = [ 41 | { 42 | id: '_id', 43 | numeric: false, 44 | disablePadding: true, 45 | label: 'ID', 46 | help: 'Container ID (truncated)' 47 | }, 48 | { 49 | id: 'name', 50 | numeric: false, 51 | disablePadding: false, 52 | label: 'Name', 53 | help: 'Container name' 54 | }, 55 | { 56 | id: 'state', 57 | numeric: false, 58 | disablePadding: false, 59 | label: 'State', 60 | help: 'Container state' 61 | }, 62 | { 63 | id: 'status', 64 | numeric: false, 65 | disablePadding: false, 66 | label: 'Status', 67 | help: 'Container status' 68 | }, 69 | { 70 | id: 'image', 71 | numeric: false, 72 | disablePadding: false, 73 | label: 'Image', 74 | help: 'Associated Image' 75 | } 76 | ] 77 | 78 | class EnhancedTable extends React.Component { 79 | state = { 80 | order: 'asc', 81 | orderBy: 'containers', 82 | selected: [], 83 | data: [], 84 | page: 0, 85 | rowsPerPage: 5, 86 | search: '' 87 | } 88 | 89 | componentDidMount() { 90 | ipcRenderer.on('container.list', this.listContainers) 91 | ipcRenderer.on('container.remove', this.containerRemove) 92 | ipcRenderer.on('container.stop', this.containerStop) 93 | ipcRenderer.send('container.list') 94 | 95 | // Automatically refresh the container list. 96 | this.refreshHandle = setInterval(() => this.onRefresh(), 2000) 97 | } 98 | 99 | componentWillUnmount() { 100 | ipcRenderer.removeListener('container.list', this.listContainers) 101 | ipcRenderer.removeListener('container.remove', this.containerRemove) 102 | ipcRenderer.removeListener('container.stop', this.containerStop) 103 | 104 | /* Remove any handles. */ 105 | clearInterval(this.refreshHandle) 106 | } 107 | 108 | listContainers = (event, containers) => { 109 | this.setState({ data: mapContainers(containers) }) 110 | } 111 | 112 | handleRequestSort = (event, property) => { 113 | const orderBy = property 114 | let order = 'desc' 115 | 116 | if (this.state.orderBy === property && this.state.order === 'desc') { 117 | order = 'asc' 118 | } 119 | 120 | this.setState({ order, orderBy }) 121 | } 122 | 123 | handleSelectAllClick = event => { 124 | if (event.target.checked) { 125 | this.setState(state => ({ selected: state.data.map(n => n.id) })) 126 | return 127 | } 128 | this.setState({ selected: [] }) 129 | } 130 | 131 | handleClick = (event, id) => { 132 | const { selected } = this.state 133 | const selectedIndex = selected.indexOf(id) 134 | let newSelected = [] 135 | 136 | if (selectedIndex === -1) { 137 | newSelected = newSelected.concat(selected, id) 138 | } else if (selectedIndex === 0) { 139 | newSelected = newSelected.concat(selected.slice(1)) 140 | } else if (selectedIndex === selected.length - 1) { 141 | newSelected = newSelected.concat(selected.slice(0, -1)) 142 | } else if (selectedIndex > 0) { 143 | newSelected = newSelected.concat( 144 | selected.slice(0, selectedIndex), 145 | selected.slice(selectedIndex + 1) 146 | ) 147 | } 148 | 149 | this.setState({ selected: newSelected }) 150 | } 151 | 152 | handleChangePage = (event, page) => { 153 | this.setState({ page }) 154 | } 155 | 156 | handleChangeRowsPerPage = event => { 157 | this.setState({ rowsPerPage: event.target.value }) 158 | } 159 | 160 | isSelected = id => { 161 | const { selected } = this.state 162 | 163 | return selected.indexOf(id) !== -1 164 | } 165 | 166 | onRemove = () => { 167 | const { selected, data } = this.state 168 | 169 | const identifiers = selected.map(entry => data[entry]._id) 170 | identifiers.map(id => ipcRenderer.send('container.remove', { id })) 171 | } 172 | 173 | containerRemove = (event, args) => { 174 | if (args.error) { 175 | logger.error(JSON.stringify(args.data)) 176 | return 177 | } 178 | this.setState({ selected: [] }) 179 | ipcRenderer.send('container.list') 180 | } 181 | 182 | onRefresh = () => { 183 | ipcRenderer.send('container.list') 184 | } 185 | 186 | onStop = () => { 187 | const { selected, data } = this.state 188 | 189 | const identifiers = selected.map(entry => data[entry]._id) 190 | identifiers.map(id => ipcRenderer.send('container.stop', { id })) 191 | } 192 | 193 | containerStop = (event, args) => { 194 | if (args.error) { 195 | logger.error(JSON.stringify(args.data)) 196 | return 197 | } 198 | this.setState({ selected: [] }) 199 | ipcRenderer.send('container.list') 200 | } 201 | 202 | onSearchChange = event => { 203 | this.setState({ search: event.target.value }) 204 | } 205 | 206 | render() { 207 | const { classes } = this.props 208 | const { data, search, order, orderBy, selected, rowsPerPage, page } = this.state 209 | 210 | return ( 211 | 212 | 220 | 235 |
236 | 237 | 246 | 247 | {stableSort(data, getSorting(order, orderBy)) 248 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 249 | .filter(item => !search || item.image.includes(search)) 250 | .map(n => { 251 | const isSelected = this.isSelected(n.id) 252 | return ( 253 | this.handleClick(event, n.id)} 256 | role="checkbox" 257 | aria-checked={isSelected} 258 | tabIndex={-1} 259 | key={n.id} 260 | selected={isSelected} 261 | > 262 | 263 | 264 | 265 | 266 | {n._id} 267 | 268 | {n.name} 269 | {n.state} 270 | {n.status} 271 | {n.image} 272 | 273 | ) 274 | })} 275 | 276 |
277 |
278 |
279 | ) 280 | } 281 | } 282 | 283 | EnhancedTable.propTypes = { 284 | classes: PropTypes.object.isRequired 285 | } 286 | 287 | export default withStyles(styles)(EnhancedTable) 288 | -------------------------------------------------------------------------------- /src/renderer/containers/Activity/DockerImages.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | /* eslint-disable no-underscore-dangle */ 3 | 4 | import React from 'react' 5 | import PropTypes from 'prop-types' 6 | import { ipcRenderer } from 'electron' 7 | 8 | /* Styles */ 9 | import { 10 | Table, 11 | TableBody, 12 | TableCell, 13 | TablePagination, 14 | TableRow, 15 | Paper, 16 | Checkbox 17 | } from '@material-ui/core' 18 | import { withStyles } from '@material-ui/core/styles' 19 | 20 | import EnhancedTableHead from './Table/TableHead' 21 | import EnhancedTableToolbar from './Table/TableToolbar' 22 | import { stableSort, getSorting } from './Table/helpers' 23 | 24 | import Logger from '../../../shared/logger' 25 | import { mapImages, formatBytes } from '../../../shared/docker' 26 | 27 | const logger = new Logger('DockerImages') 28 | 29 | // eslint-disable-next-line no-unused-vars 30 | const styles = theme => ({ 31 | root: { 32 | width: '100%' 33 | }, 34 | table: {}, 35 | tableWrapper: { 36 | overflowX: 'auto' 37 | } 38 | }) 39 | 40 | const rows = [ 41 | { 42 | id: '_id', 43 | numeric: false, 44 | disablePadding: true, 45 | label: 'ID', 46 | help: 'Image ID (truncated)' 47 | }, 48 | { 49 | id: 'size', 50 | numeric: false, 51 | disablePadding: false, 52 | label: 'Size', 53 | help: 'Image size' 54 | }, 55 | { 56 | id: 'date', 57 | numeric: true, 58 | disablePadding: false, 59 | label: 'Date', 60 | help: 'Creation date' 61 | }, 62 | { 63 | id: 'tags', 64 | numeric: false, 65 | disablePadding: false, 66 | label: 'Tags', 67 | help: 'Image tags' 68 | }, 69 | { 70 | id: 'containers', 71 | numeric: true, 72 | disablePadding: false, 73 | label: 'Containers', 74 | help: 'Running containers' 75 | } 76 | ] 77 | 78 | class EnhancedTable extends React.Component { 79 | state = { 80 | order: 'asc', 81 | orderBy: 'containers', 82 | selected: [], 83 | data: [], 84 | page: 0, 85 | rowsPerPage: 5, 86 | search: '' 87 | } 88 | 89 | componentDidMount() { 90 | ipcRenderer.on('image.list', this.listImages) 91 | ipcRenderer.on('image.remove', this.imageRemove) 92 | ipcRenderer.send('image.list') 93 | 94 | // Automatically refresh the image list. 95 | this.refreshHandle = setInterval(() => this.onRefresh(), 2000) 96 | } 97 | 98 | componentWillUnmount() { 99 | ipcRenderer.removeListener('image.list', this.listImages) 100 | ipcRenderer.removeListener('image.remove', this.imageRemove) 101 | 102 | /* Remove any handles. */ 103 | clearInterval(this.refreshHandle) 104 | } 105 | 106 | listImages = (event, images) => { 107 | this.setState({ data: mapImages(images) }) 108 | } 109 | 110 | handleRequestSort = (event, property) => { 111 | const orderBy = property 112 | let order = 'desc' 113 | 114 | if (this.state.orderBy === property && this.state.order === 'desc') { 115 | order = 'asc' 116 | } 117 | 118 | this.setState({ order, orderBy }) 119 | } 120 | 121 | handleSelectAllClick = event => { 122 | if (event.target.checked) { 123 | this.setState(state => ({ selected: state.data.map(n => n.id) })) 124 | return 125 | } 126 | this.setState({ selected: [] }) 127 | } 128 | 129 | handleClick = (event, id) => { 130 | const { selected } = this.state 131 | const selectedIndex = selected.indexOf(id) 132 | let newSelected = [] 133 | 134 | if (selectedIndex === -1) { 135 | newSelected = newSelected.concat(selected, id) 136 | } else if (selectedIndex === 0) { 137 | newSelected = newSelected.concat(selected.slice(1)) 138 | } else if (selectedIndex === selected.length - 1) { 139 | newSelected = newSelected.concat(selected.slice(0, -1)) 140 | } else if (selectedIndex > 0) { 141 | newSelected = newSelected.concat( 142 | selected.slice(0, selectedIndex), 143 | selected.slice(selectedIndex + 1) 144 | ) 145 | } 146 | 147 | this.setState({ selected: newSelected }) 148 | } 149 | 150 | handleChangePage = (event, page) => { 151 | this.setState({ page }) 152 | } 153 | 154 | handleChangeRowsPerPage = event => { 155 | this.setState({ rowsPerPage: event.target.value }) 156 | } 157 | 158 | isSelected = id => { 159 | const { selected } = this.state 160 | 161 | return selected.indexOf(id) !== -1 162 | } 163 | 164 | onRemove = () => { 165 | const { selected, data } = this.state 166 | 167 | const identifiers = selected.map(entry => data[entry]._id) 168 | identifiers.map(id => ipcRenderer.send('image.remove', { name: id })) 169 | } 170 | 171 | imageRemove = (event, args) => { 172 | if (args.error) { 173 | logger.error(JSON.stringify(args.data)) 174 | return 175 | } 176 | this.setState({ selected: [] }) 177 | ipcRenderer.send('image.list') 178 | } 179 | 180 | onRefresh = () => { 181 | ipcRenderer.send('image.list') 182 | } 183 | 184 | onSearchChange = event => { 185 | this.setState({ search: event.target.value }) 186 | } 187 | 188 | render() { 189 | const { classes } = this.props 190 | const { data, search, order, orderBy, selected, rowsPerPage, page } = this.state 191 | 192 | return ( 193 | 194 | 201 | 216 |
217 | 218 | 227 | 228 | 229 | {stableSort(data, getSorting(order, orderBy)) 230 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 231 | .filter(item => !search || item.tags.join(', ').includes(search)) 232 | .map(n => { 233 | const isSelected = this.isSelected(n.id) 234 | return ( 235 | this.handleClick(event, n.id)} 238 | role="checkbox" 239 | aria-checked={isSelected} 240 | tabIndex={-1} 241 | key={n.id} 242 | selected={isSelected} 243 | > 244 | 245 | 246 | 247 | 248 | {n._id} 249 | 250 | {formatBytes(n.size)} 251 | {n.date.toLocaleDateString()} 252 | {n.tags.join(', ')} 253 | {n.containers} 254 | 255 | ) 256 | })} 257 | 258 |
259 |
260 |
261 | ) 262 | } 263 | } 264 | 265 | EnhancedTable.propTypes = { 266 | classes: PropTypes.object.isRequired 267 | } 268 | 269 | export default withStyles(styles)(EnhancedTable) 270 | -------------------------------------------------------------------------------- /src/renderer/containers/Activity/Table/TableHead.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | /* Styles */ 6 | import Checkbox from '@material-ui/core/Checkbox' 7 | import TableHead from '@material-ui/core/TableHead' 8 | import TableCell from '@material-ui/core/TableCell' 9 | import TableRow from '@material-ui/core/TableRow' 10 | import TableSortLabel from '@material-ui/core/TableSortLabel' 11 | import Tooltip from '@material-ui/core/Tooltip' 12 | 13 | class EnhancedTableHead extends React.Component { 14 | createSortHandler = property => event => { 15 | const { onRequestSort } = this.props 16 | 17 | onRequestSort(event, property) 18 | } 19 | 20 | render() { 21 | const { onSelectAllClick, order, orderBy, numSelected, rowCount, rows } = this.props 22 | 23 | return ( 24 | 25 | 26 | 27 | 0 && numSelected < rowCount} 29 | checked={numSelected === rowCount} 30 | onChange={onSelectAllClick} 31 | /> 32 | 33 | {rows.map( 34 | row => ( 35 | 41 | 42 | 47 | {row.label} 48 | 49 | 50 | 51 | ), 52 | this 53 | )} 54 | 55 | 56 | ) 57 | } 58 | } 59 | 60 | EnhancedTableHead.propTypes = { 61 | numSelected: PropTypes.number.isRequired, 62 | onRequestSort: PropTypes.func.isRequired, 63 | onSelectAllClick: PropTypes.func.isRequired, 64 | order: PropTypes.string.isRequired, 65 | orderBy: PropTypes.string.isRequired, 66 | rowCount: PropTypes.number.isRequired, 67 | rows: PropTypes.array.isRequired 68 | } 69 | 70 | export default EnhancedTableHead 71 | -------------------------------------------------------------------------------- /src/renderer/containers/Activity/Table/TableToolbar.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | /* Styles */ 7 | import Toolbar from '@material-ui/core/Toolbar' 8 | import Typography from '@material-ui/core/Typography' 9 | import IconButton from '@material-ui/core/IconButton' 10 | import InputAdornment from '@material-ui/core/InputAdornment' 11 | import TextField from '@material-ui/core/TextField' 12 | import Tooltip from '@material-ui/core/Tooltip' 13 | import DeleteIcon from '@material-ui/icons/Delete' 14 | import RefreshIcon from '@material-ui/icons/Refresh' 15 | import SearchIcon from '@material-ui/icons/Search' 16 | import StopIcon from '@material-ui/icons/Stop' 17 | import FilterListIcon from '@material-ui/icons/FilterList' 18 | import { lighten } from '@material-ui/core/styles/colorManipulator' 19 | import { withStyles } from '@material-ui/core/styles' 20 | 21 | const styles = theme => ({ 22 | root: { 23 | paddingRight: theme.spacing.unit, 24 | background: theme.tableBar.background 25 | }, 26 | highlight: 27 | theme.palette.type === 'light' 28 | ? { 29 | color: theme.palette.secondary.main, 30 | backgroundColor: lighten(theme.palette.secondary.light, 0.85) 31 | } 32 | : { 33 | color: theme.palette.text.primary, 34 | backgroundColor: theme.palette.secondary.dark 35 | }, 36 | title: { 37 | flex: '0 0 auto' 38 | }, 39 | spacer: { 40 | flex: '1 1 100%' 41 | }, 42 | actions: { 43 | color: theme.palette.text.secondary, 44 | display: 'flex', 45 | justifyContent: 'flex-end', 46 | flex: '1 1 30%' 47 | } 48 | }) 49 | 50 | const EnhancedTableToolbar = props => { 51 | const { 52 | classes, 53 | search, 54 | numSelected, 55 | onRemoveCallback, 56 | onStopCallback, 57 | onRefreshListCallback, 58 | onFilterListCallback, 59 | onSearchCallback 60 | } = props 61 | 62 | return ( 63 | 0 ? classes.highlight : ''} `}> 64 |
65 | {numSelected > 0 ? ( 66 | 67 | {numSelected} selected 68 | 69 | ) : ( 70 | 71 | {onSearchCallback && ( 72 | 78 | 79 | 80 | ) 81 | }} 82 | /> 83 | )} 84 | 85 | )} 86 |
87 |
88 |
89 | {numSelected > 0 ? ( 90 | 91 | {onStopCallback && ( 92 | 93 | 94 | 95 | 96 | 97 | )} 98 | 99 | 100 | 101 | 102 | 103 | 104 | ) : ( 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | )} 118 |
119 | 120 | ) 121 | } 122 | 123 | EnhancedTableToolbar.propTypes = { 124 | classes: PropTypes.object.isRequired, 125 | numSelected: PropTypes.number.isRequired, 126 | onRemoveCallback: PropTypes.func.isRequired, 127 | onRefreshListCallback: PropTypes.func.isRequired, 128 | onFilterListCallback: PropTypes.func, 129 | onStopCallback: PropTypes.func, 130 | onSearchCallback: PropTypes.func, 131 | search: PropTypes.string 132 | } 133 | 134 | export default withStyles(styles)(EnhancedTableToolbar) 135 | -------------------------------------------------------------------------------- /src/renderer/containers/Activity/Table/helpers.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const desc = (a, b, orderBy) => { 4 | if (b[orderBy] < a[orderBy]) { 5 | return -1 6 | } 7 | if (b[orderBy] > a[orderBy]) { 8 | return 1 9 | } 10 | return 0 11 | } 12 | export const stableSort = (array, cmp) => { 13 | const stabilizedThis = array.map((el, index) => [el, index]) 14 | 15 | stabilizedThis.sort((a, b) => { 16 | const order = cmp(a[0], b[0]) 17 | if (order !== 0) { 18 | return order 19 | } 20 | return a[1] - b[1] 21 | }) 22 | return stabilizedThis.map(el => el[0]) 23 | } 24 | 25 | export const getSorting = (order, orderBy) => { 26 | // eslint-disable-next-line prettier/prettier 27 | return order === 'desc' 28 | ? (a, b) => desc(a, b, orderBy) 29 | : (a, b) => -desc(a, b, orderBy) 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/containers/Activity/index.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState } from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | /* Styles */ 7 | import Typography from '@material-ui/core/Typography' 8 | import Paper from '@material-ui/core/Paper' 9 | import AppBar from '@material-ui/core/AppBar' 10 | import Tabs from '@material-ui/core/Tabs' 11 | import Tab from '@material-ui/core/Tab' 12 | import { withStyles } from '@material-ui/core/styles' 13 | 14 | import DockerImages from './DockerImages' 15 | import DockerContainers from './DockerContainers' 16 | 17 | // eslint-disable-next-line no-unused-vars 18 | const styles = theme => ({}) 19 | 20 | const TabContainer = ({ children }) => { 21 | return {children} 22 | } 23 | 24 | const ActivityPage = ({ classes }) => { 25 | const [value, setValue] = useState(0) 26 | 27 | const handleChange = (event, newValue) => { 28 | setValue(newValue) 29 | } 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {value === 0 && ( 40 | 41 | 42 | 43 | )} 44 | {value === 1 && ( 45 | 46 | 47 | 48 | )} 49 | 50 | ) 51 | } 52 | 53 | ActivityPage.propTypes = { 54 | classes: PropTypes.shape({ 55 | children: PropTypes.node 56 | }) 57 | } 58 | 59 | export default withStyles(styles)(ActivityPage) 60 | -------------------------------------------------------------------------------- /src/renderer/containers/App.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { HashRouter } from 'react-router-dom' 5 | import { Provider } from 'react-redux' 6 | 7 | // eslint-disable-next-line no-unused-vars 8 | import Sentry from '../../shared/sentry' 9 | import ContentRoutes from './ContentRoutes' 10 | import SideDrawer from './Layout/SideDrawer' 11 | import SideDrawerList from './Layout/SideDrawerList' 12 | import ThemeProvider from './Themes/ThemeProvider' 13 | import { initState } from '../store' 14 | import ErrorBoundary from '../components/Error/ErrorBoundary' 15 | 16 | const App = () => { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default App 33 | -------------------------------------------------------------------------------- /src/renderer/containers/ContentRoutes.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { Route, Switch } from 'react-router-dom' 5 | 6 | /* Pages */ 7 | import DashboardPage from './Dashboard' 8 | import ActivityPage from './Activity' 9 | import PreferencesPage from './Preferences' 10 | 11 | export const ROUTES_ITEMS = [ 12 | { 13 | to: '/', 14 | exact: true, 15 | component: DashboardPage, 16 | text: 'Dashboard' 17 | }, 18 | { 19 | to: '/activity', 20 | exact: true, 21 | component: ActivityPage, 22 | text: 'Activity' 23 | }, 24 | { 25 | to: '/preferences', 26 | exact: true, 27 | component: PreferencesPage, 28 | text: 'Preferences' 29 | }, 30 | { 31 | to: '*', 32 | component: DashboardPage 33 | } 34 | ] 35 | 36 | const ROUTES = ROUTES_ITEMS.map(route => ( 37 | 38 | )) 39 | 40 | const ContentRoutes = () => { 41 | return {ROUTES} 42 | } 43 | 44 | export default ContentRoutes 45 | -------------------------------------------------------------------------------- /src/renderer/containers/Dashboard/RunTimer.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | import { bindActionCreators } from 'redux' 6 | import { connect } from 'react-redux' 7 | 8 | /* Styles */ 9 | import Typography from '@material-ui/core/Typography' 10 | import Grid from '@material-ui/core/Grid' 11 | import IconButton from '@material-ui/core/IconButton' 12 | import PlayCircleFilled from '@material-ui/icons/PlayCircleFilled' 13 | import PauseCircleFilled from '@material-ui/icons/PauseCircleFilled' 14 | import Stop from '@material-ui/icons/Stop' 15 | import { withStyles } from '@material-ui/core/styles' 16 | 17 | /* Custom UI */ 18 | import TimeCounter from '../../components/TimeCounter' 19 | 20 | const styles = theme => ({ 21 | controlButton: { 22 | fontSize: '64px' 23 | } 24 | }) 25 | 26 | const STOPPED = -1 27 | const RUNNING = 0 28 | const PAUSED = 1 29 | 30 | class RunTimer extends React.Component { 31 | getInitialState() { 32 | return { 33 | state: STOPPED, 34 | delta: 0, 35 | elapsed: 0, 36 | start: 0, 37 | handle: -1 38 | } 39 | } 40 | 41 | state = this.getInitialState() 42 | 43 | componentDidMount() { 44 | const { elapsed, status, setStatus } = this.props 45 | 46 | /* Re-initialize timer with previous time spent. */ 47 | if (status.state === RUNNING) { 48 | setStatus({ 49 | handle: setInterval(this.update, 1000), 50 | state: RUNNING, 51 | elapsed 52 | }) 53 | } 54 | } 55 | 56 | componentDidUpdate(prevProps, prevState) { 57 | const { status } = this.props 58 | 59 | /* 60 | * This needs serious improvements. The setInterval() handle needs to get destroyed 61 | * if no callback was fired. Which can be the case if the container was terminated 62 | * outside of the normal workflow, i.e by terminating the container in the Activity 63 | * tab or outside of the application. 64 | */ 65 | if (status.state === STOPPED) { 66 | clearInterval(status.handle) 67 | } 68 | } 69 | 70 | componentWillUnmount() { 71 | const { status, setStatus } = this.props 72 | 73 | clearInterval(status.handle) 74 | 75 | /* Save current spent time for a late re-initialization. */ 76 | if (status.state === RUNNING) { 77 | setStatus({ elapsed: status.delta }) 78 | } 79 | } 80 | 81 | onStart = () => { 82 | const { startCallback, setStatus, status } = this.props 83 | 84 | /* Trigger Qualifications */ 85 | if (status.state === STOPPED) { 86 | startCallback() 87 | setStatus({ 88 | start: Date.now(), 89 | handle: setInterval(this.update, 1000), 90 | state: RUNNING 91 | }) 92 | } 93 | } 94 | 95 | onStop = () => { 96 | const { stopCallback, setStatus, status } = this.props 97 | 98 | /* Trigger Qualifications */ 99 | if (status.state === RUNNING || status.state === PAUSED) { 100 | stopCallback() 101 | clearInterval(status.handle) 102 | setStatus({ ...this.getInitialState() }) 103 | } 104 | } 105 | 106 | onPause = () => { 107 | const { pauseCallback, setStatus, status } = this.props 108 | 109 | /* Trigger Qualifications */ 110 | if (status.state === RUNNING) { 111 | pauseCallback() 112 | clearInterval(status.handle) 113 | setStatus({ 114 | state: PAUSED, 115 | elapsed: status.delta 116 | }) 117 | } 118 | } 119 | 120 | onResume = () => { 121 | const { resumeCallback, setStatus, status } = this.props 122 | 123 | /* Trigger Qualifications */ 124 | if (status.state === PAUSED) { 125 | resumeCallback() 126 | setStatus({ 127 | start: Date.now() - status.elapsed, 128 | handle: setInterval(this.update, 1000), 129 | state: RUNNING 130 | }) 131 | } 132 | } 133 | 134 | update = () => { 135 | const { setStatus, status } = this.props 136 | 137 | if (status.state === RUNNING) { 138 | setStatus({ delta: Date.now() - (status.start + status.elapsed) + status.elapsed }) 139 | } 140 | } 141 | 142 | render() { 143 | const { classes, status, disabled } = this.props 144 | 145 | const isStopped = status.state === STOPPED 146 | const isRunning = status.state === RUNNING 147 | const isPaused = status.state === PAUSED 148 | 149 | return ( 150 | 151 | 152 | {isRunning || isPaused ? ( 153 | 154 | 155 | 156 | ) : null} 157 | 158 | 159 | 160 | 161 | {isStopped ? ( 162 | 163 | 164 | 165 | ) : null} 166 | 167 | {isPaused ? ( 168 | 169 | 170 | 171 | ) : null} 172 | 173 | {isRunning ? ( 174 | 175 | 176 | 177 | ) : null} 178 | 179 | 180 | {isRunning || isPaused ? ( 181 | 182 | 183 | 184 | ) : null} 185 | 186 | 187 | 188 | 189 | ) 190 | } 191 | } 192 | 193 | RunTimer.propTypes = { 194 | classes: PropTypes.object.isRequired, 195 | stopCallback: PropTypes.func.isRequired, 196 | startCallback: PropTypes.func.isRequired, 197 | pauseCallback: PropTypes.func.isRequired, 198 | resumeCallback: PropTypes.func.isRequired, 199 | setStatus: PropTypes.func.isRequired, 200 | disabled: PropTypes.bool.isRequired, 201 | status: PropTypes.object.isRequired, 202 | elapsed: PropTypes.number 203 | } 204 | 205 | const mapStateToProps = state => { 206 | return {} 207 | } 208 | 209 | const mapDispatchToProps = dispatch => { 210 | return bindActionCreators({}, dispatch) 211 | } 212 | 213 | export default connect( 214 | mapStateToProps, 215 | mapDispatchToProps 216 | )(withStyles(styles)(RunTimer)) 217 | -------------------------------------------------------------------------------- /src/renderer/containers/Dashboard/TaskInfo.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { ipcRenderer } from 'electron' 5 | import { withStyles } from '@material-ui/core' 6 | 7 | import Logger from '../../../shared/logger' 8 | import Grid from '@material-ui/core/Grid' 9 | import Typography from '@material-ui/core/Typography' 10 | 11 | const logger = new Logger('TaskInfo') 12 | 13 | // eslint-disable-next-line no-unused-vars 14 | const styles = theme => ({ 15 | root: {} 16 | }) 17 | 18 | class TaskInfo extends React.Component { 19 | state = { 20 | info: {} 21 | } 22 | 23 | componentDidMount() { 24 | ipcRenderer.on('container.stats', this.setTaskInfo) 25 | } 26 | 27 | componentWillUnmount() { 28 | this.stopRequestTaskInfo() 29 | ipcRenderer.removeListener('container.stats', this.setTaskInfo) 30 | } 31 | 32 | setTaskInfo = (event, data) => { 33 | if (data.error) { 34 | this.stopRequestTaskInfo() 35 | } 36 | if (data) { 37 | this.setState({ info: data.data }) 38 | } 39 | } 40 | 41 | requestTaskInfo = containerId => { 42 | const id = containerId || this.props.id 43 | 44 | if (!id) { 45 | logger.info(`No container is running at the moment.`) 46 | return 47 | } 48 | 49 | logger.info(`Starting TaskInfo scheduler for container: ${id}`) 50 | this.scheduler = setInterval(() => { 51 | ipcRenderer.send('container.stats', { 52 | serializerName: 'LibFuzzer', 53 | id 54 | }) 55 | }, 5000) 56 | } 57 | 58 | stopRequestTaskInfo = () => { 59 | if (!this.scheduler) { 60 | return 61 | } 62 | logger.info(`Removing container stats scheduler.`) 63 | clearInterval(this.scheduler) 64 | this.scheduler = null 65 | } 66 | 67 | render() { 68 | const { state, id, taskDefinition } = this.props 69 | 70 | if (!taskDefinition.hasOwnProperty('type')) { 71 | return null 72 | } 73 | if (taskDefinition.type !== 'LibFuzzer') { 74 | logger.warning('No task information available for this type task.') 75 | return null 76 | } 77 | 78 | if (this.scheduler && state !== 0) { 79 | logger.info('Stopping request task info.') 80 | this.stopRequestTaskInfo() 81 | } 82 | if (!this.scheduler && state === 0 && id) { 83 | logger.info('Requesting task info.') 84 | this.requestTaskInfo(id) 85 | } 86 | 87 | return DisplayLibFuzzerInfo(this.state.info) 88 | } 89 | } 90 | 91 | const numberWithCommas = str => { 92 | const n = parseInt(str) 93 | return !isNaN(n) ? n.toLocaleString() : 'N/A' 94 | } 95 | 96 | const DisplayLibFuzzerInfo = data => { 97 | const requiredData = { 98 | crashes: 'N/A', 99 | execs_done: 'N/A', 100 | execs_per_sec: 'N/A', 101 | ooms: 'N/A' 102 | } 103 | 104 | if (!data) { 105 | return 106 | } 107 | 108 | const receivedKeys = Object.keys(data) 109 | 110 | Object.keys(requiredData).forEach(name => { 111 | if (receivedKeys.includes(name)) { 112 | requiredData[name] = data[name] 113 | } 114 | }) 115 | 116 | return ( 117 | 118 | 119 | 120 | Test-cases 121 | 122 | 123 | 124 | {numberWithCommas(requiredData.execs_done)} ( 125 | {numberWithCommas(requiredData.execs_per_sec)} / sec) 126 | 127 | 128 | 129 | 130 | 131 | Crashes 132 | 133 | 134 | {numberWithCommas(requiredData.crashes)} 135 | 136 | 137 | 138 | 139 | Out-of-Memory 140 | 141 | 142 | {numberWithCommas(requiredData.ooms)} 143 | 144 | 145 | 146 | ) 147 | } 148 | 149 | export default withStyles(styles)(TaskInfo) 150 | -------------------------------------------------------------------------------- /src/renderer/containers/Dashboard/index.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import PropTypes from 'prop-types' 7 | import { ipcRenderer } from 'electron' 8 | import axios from 'axios' 9 | import { random } from '@mozillasecurity/octo' 10 | 11 | /* Styles */ 12 | import Grid from '@material-ui/core/Grid' 13 | import CircularProgress from '@material-ui/core/CircularProgress' 14 | import { withStyles } from '@material-ui/core/styles' 15 | 16 | /* Custom UI */ 17 | import Logo from '../../images/virgo-full.svg' 18 | import RunTimer from './RunTimer' 19 | import TaskInfo from './TaskInfo' 20 | 21 | import Logger from '../../../shared/logger' 22 | import * as actionCreators from '../../store/actions' 23 | import FuzzManagerConf from '../../../shared/fuzzmanager' 24 | 25 | const logger = new Logger('Dashboard') 26 | 27 | // eslint-disable-next-line no-unused-vars 28 | const styles = theme => ({ 29 | root: { 30 | flexGrow: 1, 31 | width: '100% !important', // Beware: https://material-ui.com/layout/grid/#negative-margin 32 | textAlign: 'center' 33 | } 34 | }) 35 | 36 | const STOPPED = -1 37 | const RUNNING = 0 38 | const PAUSED = 1 39 | 40 | class DashboardPage extends React.Component { 41 | async componentDidMount() { 42 | random.init() 43 | await this.fetchTaskDefinitions() 44 | 45 | this.startInspectScheduler() 46 | 47 | ipcRenderer.on('image.pull', this.pullImage) 48 | ipcRenderer.on('container.run', this.runContainer) 49 | ipcRenderer.on('container.inspect', this.inspectContainer) 50 | ipcRenderer.on('container.stop', this.stopContainer) 51 | ipcRenderer.on('container.pause', this.pauseContainer) 52 | ipcRenderer.on('container.unpause', this.unpauseContainer) 53 | ipcRenderer.on('image.error', this.imageError) 54 | ipcRenderer.on('container.error', this.containerError) 55 | } 56 | 57 | componentWillUnmount() { 58 | this.stopInspectScheduler() 59 | 60 | ipcRenderer.removeListener('image.pull', this.pullImage) 61 | ipcRenderer.removeListener('container.run', this.runContainer) 62 | ipcRenderer.removeListener('container.inspect', this.inspectContainer) 63 | ipcRenderer.removeListener('container.stop', this.stopContainer) 64 | ipcRenderer.removeListener('container.pause', this.pauseContainer) 65 | ipcRenderer.removeListener('container.unpause', this.unpauseContainer) 66 | ipcRenderer.removeListener('image.error', this.imageError) 67 | ipcRenderer.removeListener('container.error', this.containerError) 68 | } 69 | 70 | /* 71 | * IPC Event Handlers 72 | */ 73 | pullImage = (event, data) => { 74 | const { setStatus } = this.props 75 | 76 | setStatus({ text: `Downloading task: ${data}` }) 77 | } 78 | 79 | runContainer = (event, container) => { 80 | const { setContainer, setStatus } = this.props 81 | 82 | setContainer(container) 83 | this.startInspectScheduler(container.id) 84 | setStatus({ text: `Task is running`, id: container.id, state: RUNNING }) 85 | this.toggleSpinner(false) 86 | } 87 | 88 | inspectContainer = (event, data) => { 89 | const { setContainerData, setContainer, setStatus, resetStatus, status } = this.props 90 | 91 | if (data.error && status.id) { 92 | let text 93 | if (data.code === 404) { 94 | text = `Task terminated previously.` 95 | } else { 96 | text = `Task stopped outside of normal workflow.` 97 | } 98 | 99 | setContainerData([]) 100 | setContainer({}) 101 | this.stopInspectScheduler() 102 | resetStatus() 103 | setStatus({ 104 | text, 105 | state: STOPPED, 106 | id: null, 107 | delta: 0 108 | }) 109 | return 110 | } 111 | 112 | setContainerData(data) 113 | } 114 | 115 | stopContainer = (event, id) => { 116 | const { setContainerData, setContainer, setStatus } = this.props 117 | 118 | setStatus({ text: `Container ${id} stopped successfully.`, state: STOPPED }) 119 | this.stopInspectScheduler() 120 | this.toggleSpinner(false) 121 | setContainerData([]) 122 | setContainer({}) 123 | } 124 | 125 | pauseContainer = (event, id) => { 126 | const { setStatus } = this.props 127 | 128 | setStatus({ text: `Container ${id} paused successfully.`, state: PAUSED }) 129 | this.toggleSpinner(false) 130 | } 131 | 132 | unpauseContainer = (event, id) => { 133 | const { setStatus } = this.props 134 | 135 | setStatus({ text: `Container ${id} resumed successfully.`, state: RUNNING }) 136 | this.toggleSpinner(false) 137 | } 138 | 139 | imageError = (event, error) => { 140 | const { setStatus } = this.props 141 | 142 | setStatus({ text: `Error: ${error.message}`, state: STOPPED }) 143 | this.toggleSpinner(false) 144 | } 145 | 146 | containerError = (event, error) => { 147 | const { setStatus, resetStatus } = this.props 148 | 149 | resetStatus() 150 | setStatus({ text: `Error: ${error.message}`, state: STOPPED }) 151 | this.toggleSpinner(false) 152 | this.stopInspectScheduler() 153 | } 154 | 155 | /* 156 | * Action initiators 157 | */ 158 | onStart = () => { 159 | const { definitions, setStatus } = this.props 160 | 161 | if (definitions.length === 0) { 162 | setStatus({ text: `No remote tasks available.` }) 163 | return 164 | } 165 | 166 | /* Retrieve a random task. */ 167 | const taskDefinition = this.analyzeSuitableTask(definitions) 168 | 169 | const fuzzmanagerconf = new FuzzManagerConf({ configName: 'fuzzmanagerconf' }) 170 | 171 | /* Mount point for our FuzzManager backend configuration. */ 172 | const volumes = [`${fuzzmanagerconf.path}:/home/worker/.fuzzmanagerconf`] 173 | 174 | /* Indicating that we treat setting `clientid` differently. */ 175 | taskDefinition.environment.push(`VIRGO=True`) 176 | 177 | this.toggleSpinner(true) 178 | setStatus({ text: `Initializing task.`, definition: taskDefinition }) 179 | ipcRenderer.send('container.run', { task: taskDefinition, volumes }) 180 | } 181 | 182 | onResume = () => { 183 | const { container, setStatus } = this.props 184 | const { id } = container 185 | 186 | if (!id) { 187 | this.toggleSpinner(false) 188 | setStatus({ text: `No container ID available.` }) 189 | return 190 | } 191 | 192 | this.toggleSpinner(true) 193 | setStatus({ text: `Resuming container with ID: ${id}` }) 194 | ipcRenderer.send('container.unpause', { id }) 195 | } 196 | 197 | onStop = () => { 198 | const { container, setStatus } = this.props 199 | const { id } = container 200 | 201 | if (!id) { 202 | this.toggleSpinner(false) 203 | setStatus({ text: `No container ID available.` }) 204 | return 205 | } 206 | 207 | this.toggleSpinner(true) 208 | setStatus({ text: `Stopping container with ID: ${id}` }) 209 | ipcRenderer.send('container.stop', { id }) 210 | } 211 | 212 | onPause = () => { 213 | const { container, setStatus } = this.props 214 | const { id } = container 215 | 216 | if (!id) { 217 | this.toggleSpinner(false) 218 | setStatus({ text: `No container ID available.` }) 219 | return 220 | } 221 | 222 | this.toggleSpinner(true) 223 | setStatus({ text: `Pausing container with ID: ${id}` }) 224 | ipcRenderer.send('container.pause', { id }) 225 | } 226 | 227 | /* 228 | * Component Actions 229 | */ 230 | startInspectScheduler = containerId => { 231 | const { status } = this.props 232 | const id = containerId || status.id 233 | 234 | if (!id) { 235 | logger.info(`No container is running at the moment.`) 236 | return 237 | } 238 | 239 | /** 240 | * If the components switches while the user pressed Stop, then the inspection scheduler 241 | * is removed. If the user switches back to this component, it will pick up the status.id 242 | * and re-initiate the scheduler, even if the container already stopped in the background. 243 | */ 244 | logger.info(`Starting inspection scheduler for container: ${id}`) 245 | this.inspectScheduler = setInterval(() => ipcRenderer.send('container.inspect', { id }), 5000) 246 | } 247 | 248 | stopInspectScheduler = () => { 249 | if (!this.inspectScheduler) { 250 | return 251 | } 252 | logger.info(`Removing inspection scheduler.`) 253 | clearInterval(this.inspectScheduler) 254 | this.inspectScheduler = null 255 | } 256 | 257 | toggleSpinner = manualToggle => { 258 | const { setStatus, status } = this.props 259 | setStatus({ showSpinner: manualToggle !== undefined ? manualToggle : !status.showSpinner }) 260 | } 261 | 262 | analyzeSuitableTask = definitions => { 263 | return random.item(definitions) 264 | } 265 | 266 | fetchTaskDefinitions() { 267 | const { taskURL, setImageDefinitions } = this.props 268 | 269 | axios 270 | .get(taskURL) 271 | .then(response => { 272 | setImageDefinitions(response.data.tasks) 273 | }) 274 | .catch(error => { 275 | logger.error(`Error fetching task definitions: ${error}`) 276 | }) 277 | } 278 | 279 | render() { 280 | const { classes, status, setStatus, definitions } = this.props 281 | 282 | return ( 283 | 284 | 285 | Virgo 286 | 287 | 297 | 298 | {status.showSpinner ? : null} 299 | 300 | {status.state === RUNNING || status.state === PAUSED ? ( 301 | 302 | ) : null} 303 | 304 | ) 305 | } 306 | } 307 | 308 | DashboardPage.propTypes = { 309 | classes: PropTypes.object.isRequired, 310 | setImageDefinitions: PropTypes.func.isRequired, 311 | setImageError: PropTypes.func.isRequired, 312 | setContainerError: PropTypes.func.isRequired, 313 | definitions: PropTypes.array.isRequired, 314 | setContainer: PropTypes.func.isRequired, 315 | setContainerData: PropTypes.func.isRequired, 316 | container: PropTypes.object, 317 | containerData: PropTypes.any, 318 | imageError: PropTypes.object, 319 | containerError: PropTypes.object, 320 | taskURL: PropTypes.string.isRequired, 321 | status: PropTypes.object.isRequired, 322 | setStatus: PropTypes.func.isRequired, 323 | resetStatus: PropTypes.func.isRequired 324 | } 325 | 326 | const mapStateToProps = state => { 327 | // return ...docker 328 | return { 329 | definitions: state.docker.definitions, 330 | imageError: state.docker.imageError, 331 | containerError: state.docker.containerError, 332 | containerData: state.docker.containerData, 333 | container: state.docker.container, 334 | taskURL: state.preferences.taskURL, 335 | contactEmail: state.preferences.contactEmail, 336 | status: state.docker.status 337 | } 338 | } 339 | 340 | const mapDispatchToProps = dispatch => { 341 | return bindActionCreators( 342 | { 343 | setImageDefinitions: actionCreators.setImageDefinitions, 344 | setImageError: actionCreators.setImageError, 345 | setContainerError: actionCreators.setContainerError, 346 | setContainerData: actionCreators.setContainerData, 347 | setContainer: actionCreators.setContainer, 348 | setStatus: actionCreators.setStatus, 349 | resetStatus: actionCreators.resetStatus 350 | }, 351 | dispatch 352 | ) 353 | } 354 | 355 | export default connect( 356 | mapStateToProps, 357 | mapDispatchToProps 358 | )(withStyles(styles)(DashboardPage)) 359 | -------------------------------------------------------------------------------- /src/renderer/containers/Layout/MenuBar.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | 5 | import { AppBar, Toolbar, Typography } from '@material-ui/core' 6 | import Menu from '@material-ui/icons/Menu' 7 | import { withStyles } from '@material-ui/core/styles' 8 | 9 | import LogoIcon from '../../components/LogoIcon' 10 | 11 | const styles = theme => ({ 12 | root: { 13 | flexGrow: 1 14 | }, 15 | appBar: { 16 | position: 'static' 17 | }, 18 | toolBar: { 19 | // Placement of elements inside the Toolbar. 20 | padding: '0px 15px 10px 15px' 21 | } 22 | }) 23 | 24 | const MenuBar = props => { 25 | const { classes, toggle, isOpen } = props 26 | 27 | return ( 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ) 39 | } 40 | 41 | export default withStyles(styles, { withTheme: true })(MenuBar) 42 | -------------------------------------------------------------------------------- /src/renderer/containers/Layout/SideDrawer.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState } from 'react' 4 | import { withRouter } from 'react-router-dom' 5 | import { bindActionCreators } from 'redux' 6 | import { connect } from 'react-redux' 7 | import PropTypes from 'prop-types' 8 | 9 | /* Styles */ 10 | import { Drawer, List, ListItem, IconButton } from '@material-ui/core' 11 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft' 12 | import { withStyles } from '@material-ui/core/styles' 13 | 14 | /* Custom UI */ 15 | import DarkmodeSwitch from '../../components/DarkmodeSwitch' 16 | import MenuBar from './MenuBar' 17 | import StatusBar from './StatusBar' 18 | import TitleBar from './TitleBar' 19 | 20 | import * as actionCreators from '../../store/actions' 21 | 22 | const styles = theme => ({ 23 | root: { 24 | flexGrow: 1, 25 | width: '100% !important', 26 | overflow: 'hidden', 27 | height: '100%', 28 | margin: 0 29 | }, 30 | content: { 31 | flexGrow: 1, 32 | // Plus 5px padding-top cause drop-shadow gets cut by AppBar. 33 | padding: '5px 15px 30px 15px', 34 | height: '85%', 35 | overflow: 'auto', 36 | background: theme.palette.background.default 37 | }, 38 | icon: { 39 | padding: '8px' 40 | }, 41 | drawerPaper: { 42 | position: 'relative', 43 | width: 240, 44 | background: theme.drawer.background 45 | }, 46 | drawerHeader: { 47 | display: 'flex', 48 | alignItems: 'flex-end', 49 | // Equal the height of the AppBar + TitleBar. 50 | height: '74px', 51 | justifyContent: 'flex-end', 52 | padding: '0px 10px 10px 0px', 53 | ...theme.mixins.Toolbar 54 | }, 55 | drawerFooter: { 56 | width: '100%', 57 | position: 'fixed', 58 | bottom: 0 59 | } 60 | }) 61 | 62 | const Content = props => { 63 | const { classes, children } = props 64 | return
{children}
65 | } 66 | 67 | const SideDrawer = props => { 68 | const { classes, items, toggleDarkMode, darkMode } = props 69 | 70 | const [isOpen, setIsOpen] = useState(false) 71 | const toggleDrawer = () => setIsOpen(!isOpen) 72 | 73 | const brand = ( 74 |
75 | 76 | 77 | 78 |
79 | ) 80 | 81 | const drawer = ( 82 | 89 | {brand} 90 |
{items}
91 |
92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 | ) 100 | 101 | return ( 102 |
103 | 104 | 105 | {drawer} 106 | 107 | 108 |
109 | ) 110 | } 111 | 112 | SideDrawer.propTypes = { 113 | classes: PropTypes.object.isRequired, 114 | items: PropTypes.node.isRequired, 115 | children: PropTypes.node.isRequired, 116 | darkMode: PropTypes.bool.isRequired, 117 | toggleDarkMode: PropTypes.func.isRequired 118 | } 119 | 120 | const mapStateToProps = state => { 121 | return { 122 | darkMode: state.preferences.darkMode, 123 | status: state.docker.status, 124 | docker: state.docker 125 | } 126 | } 127 | 128 | const mapDispatchToProps = dispatch => { 129 | return bindActionCreators( 130 | { 131 | toggleDarkMode: actionCreators.toggleDarkMode 132 | }, 133 | dispatch 134 | ) 135 | } 136 | 137 | // Blocked Updates: https://bit.ly/2DajltC 138 | export default withRouter( 139 | connect( 140 | mapStateToProps, 141 | mapDispatchToProps 142 | )(withStyles(styles, { withTheme: true })(SideDrawer)) 143 | ) 144 | -------------------------------------------------------------------------------- /src/renderer/containers/Layout/SideDrawerList.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { Link } from 'react-router-dom' 5 | 6 | /* Styles */ 7 | import List from '@material-ui/core/List' 8 | import ListItem from '@material-ui/core/ListItem' 9 | import ListItemIcon from '@material-ui/core/ListItemIcon' 10 | import ListItemText from '@material-ui/core/ListItemText' 11 | 12 | /* Custom UI */ 13 | import DashboardIcon from '@material-ui/icons/Dashboard' 14 | import ActivityIcon from '@material-ui/icons/Report' 15 | import SettingsIcon from '@material-ui/icons/Build' 16 | 17 | const DashboardLink = props => 18 | const ActivityLink = props => 19 | const PreferencesLink = props => 20 | 21 | const SideDrawerList = () => { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ) 44 | } 45 | 46 | export default SideDrawerList 47 | -------------------------------------------------------------------------------- /src/renderer/containers/Layout/StatusBar.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | import { Grid, Paper, Typography } from '@material-ui/core' 7 | import { withStyles } from '@material-ui/core/styles' 8 | 9 | const styles = theme => ({ 10 | root: { 11 | flexGrow: 1, 12 | bottom: 0, 13 | width: '100%', 14 | position: 'fixed' 15 | }, 16 | content: { 17 | padding: '4px 10px 4px 10px', 18 | height: '28px', 19 | background: theme.statusBar.background 20 | } 21 | }) 22 | 23 | const StatusBar = props => { 24 | const { classes, docker } = props 25 | // NOTE: https://material-ui.com/layout/grid/#negative-margin 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 | {docker.status.text} 33 | 34 | 35 | 36 | 37 | 38 | 39 | {docker.definitions.length} Tasks 40 | 41 | 42 | 43 | 44 |
45 | ) 46 | } 47 | 48 | StatusBar.propTypes = { 49 | classes: PropTypes.object.isRequired, 50 | docker: PropTypes.object.isRequired 51 | } 52 | 53 | export default withStyles(styles, { withTheme: true })(StatusBar) 54 | -------------------------------------------------------------------------------- /src/renderer/containers/Layout/TitleBar.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import React from 'react' 3 | 4 | import { withStyles } from '@material-ui/core/styles' 5 | 6 | const styles = theme => ({ 7 | root: { 8 | flexGrow: 1 9 | }, 10 | titleBar: { 11 | width: '100%', 12 | height: '25px', 13 | userSelect: 'none', 14 | appRegion: 'drag', 15 | position: 'static' 16 | } 17 | }) 18 | 19 | const TitleBar = props => { 20 | const { classes } = props 21 | 22 | return ( 23 |
24 |
25 |
26 | ) 27 | } 28 | 29 | export default withStyles(styles, { withTheme: true })(TitleBar) 30 | -------------------------------------------------------------------------------- /src/renderer/containers/Preferences/Appearance.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import React from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import PropTypes from 'prop-types' 6 | 7 | /* Styles */ 8 | import List from '@material-ui/core/List' 9 | import ListItem from '@material-ui/core/ListItem' 10 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 11 | import ListItemText from '@material-ui/core/ListItemText' 12 | import Divider from '@material-ui/core/Divider' 13 | import Switch from '@material-ui/core/Switch' 14 | import { withStyles } from '@material-ui/core/styles' 15 | 16 | import {Environment} from '../../../shared/common' 17 | 18 | /* Custom UI */ 19 | import DarkmodeSwitch from '../../components/DarkmodeSwitch' 20 | 21 | import * as actionCreators from '../../store/actions' 22 | 23 | // eslint-disable-next-line no-unused-vars 24 | const styles = theme => ({}) 25 | 26 | const AppearancePrefs = props => { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | /* Prop Types */ 85 | AppearancePrefs.propTypes = { 86 | darkMode: PropTypes.bool.isRequired, 87 | vibrance: PropTypes.bool.isRequired, 88 | restoreWindowSize: PropTypes.bool.isRequired, 89 | toggleDarkMode: PropTypes.func.isRequired, 90 | toggleVibrance: PropTypes.func.isRequired, 91 | toggleRestoreWindowSize: PropTypes.func.isRequired, 92 | toggleAlwaysOnTop: PropTypes.func.isRequired 93 | } 94 | 95 | /* States */ 96 | const mapStateToProps = state => { 97 | return { 98 | darkMode: state.preferences.darkMode, 99 | vibrance: state.preferences.vibrance, 100 | restoreWindowSize: state.preferences.restoreWindowSize, 101 | alwaysOnTop: state.preferences.alwaysOnTop 102 | } 103 | } 104 | 105 | /* Dispatchers */ 106 | const mapDispatchToProps = dispatch => { 107 | return bindActionCreators( 108 | { 109 | toggleDarkMode: actionCreators.toggleDarkMode, 110 | toggleVibrance: actionCreators.toggleVibrance, 111 | toggleRestoreWindowSize: actionCreators.toggleRestoreWindowSize, 112 | toggleAlwaysOnTop: actionCreators.toggleAlwaysOnTop 113 | }, 114 | dispatch 115 | ) 116 | } 117 | 118 | /* Connect to to Redux */ 119 | export default connect( 120 | mapStateToProps, 121 | mapDispatchToProps 122 | )(withStyles(styles)(AppearancePrefs)) 123 | -------------------------------------------------------------------------------- /src/renderer/containers/Preferences/Backends/FuzzManager.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import PropTypes from 'prop-types' 7 | 8 | import List from '@material-ui/core/List' 9 | import ListItem from '@material-ui/core/ListItem' 10 | import TextField from '@material-ui/core/TextField' 11 | import { withStyles } from '@material-ui/core' 12 | 13 | import { PortValidator, ProtocolValidator } from '../../../lib/validators' 14 | import * as actionCreators from '../../../store/actions' 15 | import Logger from '../../../../shared/logger' 16 | import FuzzManagerConf from '../../../../shared/fuzzmanager' 17 | 18 | const logger = new Logger('Prefs.FuzzManager') 19 | 20 | // eslint-disable-next-line no-unused-vars 21 | const styles = theme => ({ 22 | updateButton: { 23 | padding: '3px' 24 | } 25 | }) 26 | 27 | const MyTextField = ({ ...props }) => { 28 | delete props.isInvalid 29 | 30 | return ( 31 | 38 | ) 39 | } 40 | 41 | class BackendFuzzManagerPrefs extends React.Component { 42 | constructor() { 43 | super() 44 | 45 | this.fuzzmanager = new FuzzManagerConf({ configName: 'fuzzmanagerconf' }) 46 | this.fuzzmanager.readFile() 47 | 48 | this.state = { 49 | host: { 50 | id: 'host', 51 | value: '', 52 | label: 'Backend Server Host', 53 | placeholder: this.fuzzmanager.get('serverhost'), 54 | error: false, 55 | helperText: 'The host to which the crash reports are sent.', 56 | isInvalid: value => (value !== '' ? false : 'This appears not to be a valid hostname.') 57 | }, 58 | port: { 59 | id: 'port', 60 | value: '', 61 | type: 'number', 62 | label: 'Backend Server Port', 63 | placeholder: this.fuzzmanager.get('serverport').toString(), 64 | error: false, 65 | helperText: 'The port number at which to contact the server.', 66 | isInvalid: value => 67 | value === '' || PortValidator.test(value) 68 | ? false 69 | : 'This appears not to a valid port number.' 70 | }, 71 | protocol: { 72 | id: 'protocol', 73 | value: '', 74 | label: 'Backend Server Protocol', 75 | placeholder: this.fuzzmanager.get('serverproto'), 76 | error: false, 77 | helperText: 'The transfer protocol.', 78 | isInvalid: value => 79 | value === '' || ProtocolValidator.test(value) 80 | ? false 81 | : 'This appears not to be a supported or valid protocol.' 82 | } 83 | } 84 | } 85 | 86 | componentWillUnmount() { 87 | this.fuzzmanager.set('clientid', this.props.contactEmail) 88 | this.fuzzmanager.saveFile() 89 | } 90 | 91 | onChange = ({ target: { id, value } }) => { 92 | const source = this.state[id] 93 | const invalid = source.isInvalid(value) 94 | 95 | this.setState({ 96 | ...this.state, 97 | [id]: { 98 | ...source, 99 | value, 100 | error: Boolean(invalid), 101 | helperText: invalid || source.helperText 102 | } 103 | }) 104 | } 105 | 106 | onBlur = ({ target: { id } }) => { 107 | const source = this.state[id] 108 | 109 | switch (id) { 110 | case 'host': 111 | if (!source.error) { 112 | this.fuzzmanager.set('serverhost', source.value || source.placeholder) 113 | } 114 | break 115 | case 'port': 116 | if (!source.error) { 117 | this.fuzzmanager.set('serverport', source.value || source.placeholder) 118 | } 119 | break 120 | case 'protocol': 121 | if (!source.error) { 122 | this.fuzzmanager.set('serverproto', source.value || source.placeholder) 123 | } 124 | break 125 | default: 126 | logger.error('Unrecognized target id for validating.') 127 | } 128 | } 129 | 130 | render() { 131 | const { host, port, protocol } = this.state 132 | const { classes } = this.props 133 | 134 | return ( 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | ) 147 | } 148 | } 149 | 150 | BackendFuzzManagerPrefs.propTypes = { 151 | classes: PropTypes.object.isRequired, 152 | contactEmail: PropTypes.string.isRequired 153 | } 154 | 155 | const mapStateToProps = state => { 156 | return { 157 | contactEmail: state.preferences.contactEmail 158 | } 159 | } 160 | 161 | const mapDispatchToProps = dispatch => { 162 | return bindActionCreators({}, dispatch) 163 | } 164 | 165 | export default connect( 166 | mapStateToProps, 167 | mapDispatchToProps 168 | )(withStyles(styles)(BackendFuzzManagerPrefs)) 169 | -------------------------------------------------------------------------------- /src/renderer/containers/Preferences/Docker.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { ipcRenderer, remote } from 'electron' 7 | import PropTypes from 'prop-types' 8 | 9 | import List from '@material-ui/core/List' 10 | import ListItem from '@material-ui/core/ListItem' 11 | import TextField from '@material-ui/core/TextField' 12 | import Grid from '@material-ui/core/Grid' 13 | import IconButton from '@material-ui/core/IconButton' 14 | import Divider from '@material-ui/core/Divider' 15 | import Tooltip from '@material-ui/core/Tooltip' 16 | import Checkbox from '@material-ui/core/Checkbox' 17 | import RefreshIcon from '@material-ui/icons/Refresh' 18 | import { withStyles, Typography } from '@material-ui/core' 19 | 20 | import * as actionCreators from '../../store/actions' 21 | import { URLValidator, EmailValidator } from '../../lib/validators' 22 | import Logger from '../../../shared/logger' 23 | import BackendFuzzManagerPrefs from './Backends/FuzzManager' 24 | 25 | const logger = new Logger('Prefs.Docker') 26 | 27 | // eslint-disable-next-line no-unused-vars 28 | const styles = theme => ({ 29 | updateButton: { 30 | padding: '3px' 31 | } 32 | }) 33 | 34 | const MyTextField = ({ ...props }) => { 35 | delete props.isInvalid 36 | 37 | return ( 38 | 45 | ) 46 | } 47 | 48 | class DockerPrefs extends React.Component { 49 | state = { 50 | taskURL: { 51 | id: 'taskURL', 52 | value: '', 53 | label: 'Task Definition Server', 54 | placeholder: this.props.taskURL, 55 | error: false, 56 | helperText: 'The server which serves the JSON encoded task definitions.', 57 | isInvalid: value => 58 | value === '' || URLValidator.test(value) ? false : 'This appears not to be a valid URL.' 59 | }, 60 | contactEmail: { 61 | id: 'contactEmail', 62 | value: '', 63 | label: 'Contact Email', 64 | placeholder: this.props.contactEmail, 65 | error: false, 66 | helperText: 'The contact address over which we shall notify you.', 67 | isInvalid: value => 68 | value === '' || EmailValidator.test(value) 69 | ? false 70 | : 'This appears not to a valid Email address.' 71 | }, 72 | updateMessage: '' 73 | } 74 | 75 | componentDidMount() { 76 | ipcRenderer.on('updateMessage', this.onUpdateMessage) 77 | } 78 | 79 | componentWillUnmount() { 80 | ipcRenderer.removeListener('updateMessage', this.onUpdateMessage) 81 | } 82 | 83 | onCheckUpdate = () => { 84 | ipcRenderer.send('updateCheck', {}) 85 | } 86 | 87 | onUpdateMessage = (event, data) => { 88 | this.setState({ updateMessage: data.msg }) 89 | } 90 | 91 | onChange = ({ target: { id, value } }) => { 92 | const source = this.state[id] 93 | const invalid = source.isInvalid(value) 94 | 95 | this.setState({ 96 | ...this.state, 97 | [id]: { 98 | ...source, 99 | value, 100 | error: Boolean(invalid), 101 | helperText: invalid || source.helperText 102 | } 103 | }) 104 | } 105 | 106 | onBlur = ({ target: { id } }) => { 107 | const source = this.state[id] 108 | 109 | switch (id) { 110 | case 'taskURL': 111 | if (!source.error) { 112 | this.props.updateTaskURL(source.value || source.placeholder) 113 | } 114 | break 115 | case 'contactEmail': 116 | if (!source.error) { 117 | this.props.updateContactEmail(source.value || source.placeholder) 118 | } 119 | break 120 | default: 121 | logger.error('Unrecognized target id for validating.') 122 | } 123 | } 124 | 125 | render() { 126 | const { updateMessage, taskURL, contactEmail } = this.state 127 | const { classes, toggleEarlyReleases, earlyReleases } = this.props 128 | 129 | return ( 130 | 131 | 132 | 133 | 134 | 135 | Current Version: {remote.app.getVersion()} 136 | 137 | 138 | 139 | 140 | 141 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | {updateMessage} 154 | 155 | 156 | 157 | 158 | 159 | Enable Early Releases 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | ) 175 | } 176 | } 177 | 178 | DockerPrefs.propTypes = { 179 | classes: PropTypes.object.isRequired, 180 | updateTaskURL: PropTypes.func.isRequired, 181 | taskURL: PropTypes.string.isRequired, 182 | updateContactEmail: PropTypes.func.isRequired, 183 | contactEmail: PropTypes.string.isRequired, 184 | earlyReleases: PropTypes.bool.isRequired, 185 | toggleEarlyReleases: PropTypes.func.isRequired 186 | } 187 | 188 | const mapStateToProps = state => { 189 | return { 190 | taskURL: state.preferences.taskURL, 191 | contactEmail: state.preferences.contactEmail, 192 | earlyReleases: state.preferences.earlyReleases, 193 | backend: state.preferences.backend 194 | } 195 | } 196 | 197 | const mapDispatchToProps = dispatch => { 198 | return bindActionCreators( 199 | { 200 | updateTaskURL: actionCreators.updateTaskURL, 201 | updateContactEmail: actionCreators.updateContactEmail, 202 | toggleEarlyReleases: actionCreators.toggleEarlyReleases 203 | }, 204 | dispatch 205 | ) 206 | } 207 | 208 | export default connect( 209 | mapStateToProps, 210 | mapDispatchToProps 211 | )(withStyles(styles)(DockerPrefs)) 212 | -------------------------------------------------------------------------------- /src/renderer/containers/Preferences/index.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState } from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | /* Styles */ 7 | import Typography from '@material-ui/core/Typography' 8 | import Paper from '@material-ui/core/Paper' 9 | import AppBar from '@material-ui/core/AppBar' 10 | import Tabs from '@material-ui/core/Tabs' 11 | import Tab from '@material-ui/core/Tab' 12 | import { withStyles } from '@material-ui/core/styles' 13 | 14 | /* Preference Pages */ 15 | import AppearancePrefs from './Appearance' 16 | import DockerPrefs from './Docker' 17 | 18 | // eslint-disable-next-line no-unused-vars 19 | const styles = theme => ({}) 20 | 21 | const TabContainer = ({ children }) => { 22 | return ( 23 | 24 | {children} 25 | 26 | ) 27 | } 28 | 29 | const PreferencesPage = ({ classes }) => { 30 | const [value, setValue] = useState(0) 31 | 32 | const handleChange = (event, newValue) => { 33 | setValue(newValue) 34 | } 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {value === 0 && ( 45 | 46 | 47 | 48 | )} 49 | {value === 1 && ( 50 | 51 | 52 | 53 | )} 54 | 55 | ) 56 | } 57 | 58 | PreferencesPage.propTypes = { 59 | classes: PropTypes.shape({ 60 | children: PropTypes.node 61 | }) 62 | } 63 | 64 | export default withStyles(styles)(PreferencesPage) 65 | -------------------------------------------------------------------------------- /src/renderer/containers/Themes/Base.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const themeBase = { 4 | typography: { 5 | fontSize: 13, 6 | useNextVariants: true 7 | } 8 | } 9 | 10 | export default themeBase 11 | -------------------------------------------------------------------------------- /src/renderer/containers/Themes/Dark.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import { merge } from 'lodash' 3 | 4 | import themeBase from './Base' 5 | 6 | const darkTheme = merge( 7 | { 8 | shadows: Array(25).fill('none'), 9 | palette: { 10 | type: 'dark', 11 | background: { 12 | default: '#1e2022' 13 | }, 14 | primary: { 15 | main: '#00bcd4' 16 | }, 17 | secondary: { 18 | main: '#ff4081' 19 | } 20 | }, 21 | overrides: { 22 | MuiToolbar: { 23 | root: { 24 | backgroundColor: '#1e2022', 25 | color: '#c8c8c8' 26 | } 27 | }, 28 | MuiAppBar: { 29 | colorPrimary: { 30 | backgroundColor: '#1e2022', 31 | boxShadow: 'none' 32 | } 33 | } 34 | }, 35 | drawer: { 36 | background: '#1e2022' 37 | }, 38 | statusBar: { 39 | background: '#2a2d2f' 40 | }, 41 | tableBar: { 42 | background: '#1b1c1e' 43 | } 44 | }, 45 | themeBase 46 | ) 47 | 48 | export default darkTheme 49 | -------------------------------------------------------------------------------- /src/renderer/containers/Themes/Light.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import { merge } from 'lodash' 3 | 4 | import themeBase from './Base' 5 | 6 | const themeLight = merge( 7 | { 8 | palette: { 9 | type: 'light', 10 | background: { 11 | default: '#fafafa' 12 | }, 13 | primary: { 14 | main: '#00bcd4' 15 | }, 16 | secondary: { 17 | main: '#ff4081' 18 | } 19 | }, 20 | overrides: { 21 | MuiToolbar: { 22 | root: { 23 | backgroundColor: '#fafafa', 24 | color: '#c8c8c8' 25 | } 26 | }, 27 | MuiAppBar: { 28 | colorPrimary: { 29 | backgroundColor: '#fafafa', 30 | boxShadow: 'none' 31 | } 32 | } 33 | }, 34 | drawer: { 35 | background: '#fafafa' 36 | }, 37 | statusBar: { 38 | background: '#fbfbfb' 39 | }, 40 | tableBar: { 41 | background: '#efefef' 42 | } 43 | }, 44 | themeBase 45 | ) 46 | 47 | export default themeLight 48 | -------------------------------------------------------------------------------- /src/renderer/containers/Themes/ThemeProvider.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { remote } from 'electron' 6 | import PropTypes from 'prop-types' 7 | 8 | /* Styles */ 9 | import { MuiThemeProvider, createMuiTheme, withStyles } from '@material-ui/core/styles' 10 | import Reset from '@material-ui/core/CssBaseline' 11 | import '../../styles/fontface-roboto.css' 12 | 13 | /* Custom UI Themes */ 14 | import themeDark from './Dark' 15 | import themeLight from './Light' 16 | import themeVibrance from './Vibrancy' 17 | 18 | const styles = () => ({ 19 | '@global': { 20 | '*, *::after, *::before': { 21 | userSelect: 'none', 22 | userDrag: 'none', 23 | cursor: 'default !important' 24 | } 25 | } 26 | }) 27 | 28 | const ThemeProvider = props => { 29 | const { toggleDarkMode, toggleVibrance, children } = props 30 | 31 | let theme = themeLight 32 | if (toggleVibrance && toggleDarkMode) { 33 | remote.getCurrentWindow().setVibrancy('dark') 34 | theme = themeVibrance 35 | } 36 | if (!toggleVibrance && toggleDarkMode) { 37 | remote.getCurrentWindow().setVibrancy('') 38 | theme = themeDark 39 | } 40 | 41 | return ( 42 | 43 | 44 | {children} 45 | 46 | ) 47 | } 48 | 49 | ThemeProvider.propTypes = { 50 | children: PropTypes.node.isRequired, 51 | toggleDarkMode: PropTypes.bool.isRequired, 52 | toggleVibrance: PropTypes.bool.isRequired 53 | } 54 | 55 | function mapStateToProps(state) { 56 | return { 57 | toggleDarkMode: state.preferences.darkMode, 58 | toggleVibrance: state.preferences.vibrance 59 | } 60 | } 61 | 62 | export default withStyles(styles)(connect(mapStateToProps)(ThemeProvider)) 63 | -------------------------------------------------------------------------------- /src/renderer/containers/Themes/Vibrancy.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import { merge } from 'lodash' 3 | 4 | import themeDark from './Dark' 5 | 6 | const vibrancyTheme = merge({}, themeDark, { 7 | palette: { 8 | background: 'transparent' 9 | }, 10 | overrides: { 11 | MuiToolbar: { 12 | root: { 13 | background: 'transparent !important' 14 | } 15 | }, 16 | MuiAppBar: { 17 | colorPrimary: { 18 | background: 'transparent !important' 19 | } 20 | } 21 | }, 22 | drawer: { 23 | background: 'rgba(40, 44, 52, 0.8)' 24 | }, 25 | statusBar: { 26 | background: 'rgba(40, 44, 52, 0.4)' 27 | }, 28 | tableBar: { 29 | background: 'rgba(40, 44, 52, 0.8)' 30 | } 31 | }) 32 | 33 | export default vibrancyTheme 34 | -------------------------------------------------------------------------------- /src/renderer/fonts/Roboto-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/src/renderer/fonts/Roboto-Italic-webfont.woff -------------------------------------------------------------------------------- /src/renderer/fonts/Roboto-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/src/renderer/fonts/Roboto-Light-webfont.woff -------------------------------------------------------------------------------- /src/renderer/fonts/Roboto-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/src/renderer/fonts/Roboto-LightItalic-webfont.woff -------------------------------------------------------------------------------- /src/renderer/fonts/Roboto-Medium-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/src/renderer/fonts/Roboto-Medium-webfont.woff -------------------------------------------------------------------------------- /src/renderer/fonts/Roboto-MediumItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/src/renderer/fonts/Roboto-MediumItalic-webfont.woff -------------------------------------------------------------------------------- /src/renderer/fonts/Roboto-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/src/renderer/fonts/Roboto-Regular-webfont.woff -------------------------------------------------------------------------------- /src/renderer/images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaSecurity/virgo/5227d2bc84fbec683e14613a4ce204a515b3be8e/src/renderer/images/test.png -------------------------------------------------------------------------------- /src/renderer/images/virgo-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/images/virgo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Virgo 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/renderer/index.jsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | 6 | import App from './containers/App' 7 | 8 | ReactDOM.render(, document.getElementById('parcel-root')) 9 | -------------------------------------------------------------------------------- /src/renderer/lib/validators.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const EmailValidator = new RegExp(/\S+@\S+\.\S+/) 4 | 5 | export const URLValidator = new RegExp( 6 | '^' + 7 | // Protocol identifier (optional) 8 | '(?:(?:(?:https?|ftp):)?\\/\\/)' + 9 | // BasicAuth user:pass (optional) 10 | '(?:\\S+(?::\\S*)?@)?' + 11 | '(?:' + 12 | // IP address exclusion 13 | // private & local networks 14 | // '(?!(?:10|127)(?:\\.\\d{1,3}){3})' + 15 | '(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' + 16 | '(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' + 17 | // IP address dotted notation octets 18 | // excludes loopback network 0.0.0.0 19 | // excludes reserved space >= 224.0.0.0 20 | // excludes network & broadcast addresses 21 | // (first & last IP address of each class) 22 | '(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' + 23 | '(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' + 24 | '(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' + 25 | '|' + 26 | // Host & domain names, may end with dot can be replaced by a shortest alternative: 27 | // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ 28 | '(?:' + 29 | '(?:' + 30 | '[a-z0-9\\u00a1-\\uffff]' + 31 | '[a-z0-9\\u00a1-\\uffff_-]{0,62}' + 32 | ')?' + 33 | '[a-z0-9\\u00a1-\\uffff]\\.' + 34 | ')+' + 35 | // TLD identifier name, may end with dot 36 | '(?:[a-z\\u00a1-\\uffff]{2,}\\.?)' + 37 | ')' + 38 | // Port number (optional) 39 | '(?::\\d{2,5})?' + 40 | // Resource path (optional) 41 | '(?:[/?#]\\S*)?' + 42 | '$', 43 | 'i' 44 | ) 45 | 46 | export const PortValidator = new RegExp( 47 | '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$' 48 | ) 49 | 50 | export const ProtocolValidator = new RegExp(/^https?$/) 51 | -------------------------------------------------------------------------------- /src/renderer/store/actions/docker.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /* Redux Actions */ 4 | 5 | export const setImageError = value => ({ 6 | type: 'IMAGE_ERROR', 7 | error: value 8 | }) 9 | 10 | export const setContainerError = value => ({ 11 | type: 'CONTAINER_ERROR', 12 | error: value 13 | }) 14 | 15 | export const setImageDefinitions = value => ({ 16 | type: 'IMAGE_DEFINITIONS', 17 | definitions: value 18 | }) 19 | 20 | export const setContainerData = value => ({ 21 | type: 'CONTAINER_DATA', 22 | containerData: value 23 | }) 24 | 25 | export const setContainer = value => ({ 26 | type: 'CONTAINER', 27 | container: value 28 | }) 29 | 30 | export const setStatus = value => ({ 31 | type: 'SET_STATUS', 32 | status: value 33 | }) 34 | 35 | export const resetStatus = () => ({ 36 | type: 'RESET_STATUS' 37 | }) 38 | 39 | export const setVisibilityFilter = filter => ({ 40 | type: 'SET_VISIBILITY_FILTER', 41 | filter 42 | }) 43 | 44 | export const VisibilityFilters = { 45 | SHOW_ALL: 'SHOW_ALL', 46 | SHOW_STOPPED: 'SHOW_STOPPED', 47 | SHOW_RUNNING: 'SHOW_RUNNING' 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/store/actions/index.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /* 4 | * The Actions of Redux. 5 | */ 6 | 7 | export * from './preferences' 8 | export * from './docker' 9 | -------------------------------------------------------------------------------- /src/renderer/store/actions/preferences.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /* Redux Actions */ 4 | 5 | export const toggleDarkMode = () => ({ 6 | type: 'PREF_DARKMODE' 7 | }) 8 | 9 | export const toggleVibrance = () => ({ 10 | type: 'PREF_VIBRANCE' 11 | }) 12 | 13 | export const toggleRestoreWindowSize = () => ({ 14 | type: 'PREF_RESTORE_WINDOW_SIZE' 15 | }) 16 | 17 | export const toggleAlwaysOnTop = () => ({ 18 | type: 'PREF_ALWAYS_ON_TOP' 19 | }) 20 | 21 | export const updateTaskURL = value => ({ 22 | type: 'PREF_UPDATE_TASK_URL', 23 | value 24 | }) 25 | 26 | export const updateContactEmail = value => ({ 27 | type: 'PREF_UPDATE_CONTACT_EMAIL', 28 | value 29 | }) 30 | 31 | export const toggleEarlyReleases = () => ({ 32 | type: 'PREF_EARLY_RELEASES_UPDATE' 33 | }) 34 | -------------------------------------------------------------------------------- /src/renderer/store/index.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | /* 3 | * The Store of Redux. 4 | * Each state in the app is maintained from this single store of truth. 5 | */ 6 | import { createStore, compose } from 'redux' 7 | import Store from 'electron-store' 8 | 9 | /* Combined Redux Reducers */ 10 | import rootReducer from './reducers' 11 | import FuzzManagerConf from '../../shared/fuzzmanager' 12 | import Logger from '../../shared/logger' 13 | 14 | const logger = new Logger('Store') 15 | 16 | export const saveState = (state, store) => { 17 | try { 18 | store.set(state) 19 | } catch (error) { 20 | logger.error(error) 21 | return false 22 | } 23 | return true 24 | } 25 | 26 | export const initState = () => { 27 | const persistentStore = new Store() 28 | 29 | /* Initial write of FuzzManager configuration with default values. */ 30 | const fuzzmanagerconf = new FuzzManagerConf({ 31 | configName: 'fuzzmanagerconf', 32 | defaults: persistentStore.store.preferences.backend.fuzzmanager 33 | }) 34 | if (!fuzzmanagerconf.exists()) { 35 | fuzzmanagerconf.saveFile() 36 | } else { 37 | fuzzmanagerconf.readFile() 38 | persistentStore.store.preferences.backend.fuzzmanager = fuzzmanagerconf.data 39 | } 40 | 41 | /* eslint-disable no-underscore-dangle */ 42 | const enhancers = compose( 43 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 44 | ) 45 | 46 | /* Hydrate */ 47 | const store = createStore(rootReducer, Object.assign({}, persistentStore.store), enhancers) 48 | 49 | store.subscribe(() => { 50 | saveState(store.getState(), persistentStore) 51 | }) 52 | return store 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/store/reducers/docker.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /* Redux Reducers */ 4 | 5 | const initialState = { 6 | definitions: [], 7 | container: {}, 8 | containerData: [], 9 | status: { 10 | id: null, 11 | state: -1, 12 | delta: 0, 13 | elapsed: 0, 14 | start: 0, 15 | text: 'Launch me!', 16 | handle: -1, 17 | showSpinner: false 18 | }, 19 | history: [] 20 | } 21 | 22 | const docker = (state = initialState, action) => { 23 | switch (action.type) { 24 | case 'IMAGE_DEFINITIONS': 25 | return { 26 | ...state, 27 | definitions: action.definitions 28 | } 29 | case 'IMAGE_ERROR': 30 | return { 31 | ...state, 32 | imageError: action.error 33 | } 34 | case 'CONTAINER_ERROR': 35 | return { 36 | ...state, 37 | containerError: action.error 38 | } 39 | case 'CONTAINER_DATA': 40 | return { 41 | ...state, 42 | containerData: action.containerData 43 | } 44 | case 'CONTAINER': 45 | return { 46 | ...state, 47 | container: action.container 48 | } 49 | case 'SET_STATUS': 50 | return { 51 | ...state, 52 | status: { 53 | ...state.status, 54 | ...action.status 55 | } 56 | } 57 | case 'RESET_STATUS': 58 | return { 59 | ...state, 60 | status: { 61 | ...initialState.status 62 | } 63 | } 64 | case 'SET_VISIBILITY_FILTER': 65 | return action.filter 66 | default: 67 | return state 68 | } 69 | } 70 | 71 | export default docker 72 | -------------------------------------------------------------------------------- /src/renderer/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { combineReducers } from 'redux' 4 | import preferences from './preferences' 5 | import docker from './docker' 6 | 7 | /* 8 | * The Reducers are the Redux dispatcher. 9 | * Each function is executed when a Redux action is dispatched. 10 | */ 11 | 12 | export default combineReducers({ 13 | preferences, 14 | docker 15 | }) 16 | -------------------------------------------------------------------------------- /src/renderer/store/reducers/preferences.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /* Redux Reducers */ 4 | 5 | const preferences = (state = {}, action) => { 6 | switch (action.type) { 7 | case 'PREF_DARKMODE': 8 | return { 9 | ...state, 10 | darkMode: !state.darkMode 11 | } 12 | case 'PREF_VIBRANCE': 13 | return { 14 | ...state, 15 | vibrance: !state.vibrance 16 | } 17 | case 'PREF_RESTORE_WINDOW_SIZE': 18 | return { 19 | ...state, 20 | restoreWindowSize: !state.restoreWindowSize 21 | } 22 | case 'PREF_ALWAYS_ON_TOP': 23 | return { 24 | ...state, 25 | alwaysOnTop: !state.alwaysOnTop 26 | } 27 | case 'PREF_UPDATE_TASK_URL': 28 | return { 29 | ...state, 30 | taskURL: action.value 31 | } 32 | case 'PREF_UPDATE_CONTACT_EMAIL': 33 | return { 34 | ...state, 35 | contactEmail: action.value 36 | } 37 | case 'PREF_EARLY_RELEASES_UPDATE': 38 | return { 39 | ...state, 40 | earlyReleases: !state.earlyReleases 41 | } 42 | default: 43 | return state 44 | } 45 | } 46 | 47 | export default preferences 48 | -------------------------------------------------------------------------------- /src/renderer/styles/fontface-roboto.css: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /* Light and Light Italic */ 4 | @font-face { 5 | font-family: 'Roboto'; 6 | src: url('../fonts/Roboto-Light-webfont.woff'); 7 | font-weight: 300; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: 'Roboto'; 13 | src: url('../fonts/Roboto-LightItalic-webfont.woff'); 14 | font-weight: 300; 15 | font-style: italic; 16 | } 17 | 18 | /* Regular and Italic */ 19 | @font-face { 20 | font-family: 'Roboto'; 21 | src: url('../fonts/Roboto-Regular-webfont.woff'); 22 | font-weight: 400; 23 | font-style: normal; 24 | } 25 | 26 | @font-face { 27 | font-family: 'Roboto'; 28 | src: url('../fonts/Roboto-Italic-webfont.woff'); 29 | font-weight: 400; 30 | font-style: italic; 31 | } 32 | 33 | /* Medium and Italic */ 34 | @font-face { 35 | font-family: 'Roboto'; 36 | src: url('../fonts/Roboto-Medium-webfont.woff'); 37 | font-weight: 500; 38 | font-style: normal; 39 | } 40 | 41 | @font-face { 42 | font-family: 'Roboto'; 43 | src: url('../fonts/Roboto-MediumItalic-webfont.woff'); 44 | font-weight: 500; 45 | font-style: italic; 46 | } 47 | -------------------------------------------------------------------------------- /src/shared/common.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import electron from 'electron' 3 | import { resolve } from 'app-root-path' 4 | import { transform, isEqual, isEqualWith, isObject } from 'lodash' 5 | 6 | // eslint-disable-next-line import/no-dynamic-require,global-require 7 | export const Package = require(resolve('./package.json')) 8 | 9 | /** 10 | * Common referenced environment functions in renderer and main. 11 | * 12 | * @class Environment 13 | */ 14 | export class Environment { 15 | static get isPackaged() { 16 | return (electron.app || electron.remote.app).isPackaged 17 | } 18 | 19 | static get isTest() { 20 | return process.env.NODE_ENV === 'test' 21 | } 22 | 23 | static get isProduction() { 24 | return process.env.NODE_ENV === 'production' 25 | } 26 | 27 | static get isDevelopment() { 28 | return process.env.NODE_ENV === 'development' 29 | } 30 | 31 | static get isMacOS() { 32 | return process.platform === 'darwin' 33 | } 34 | 35 | static get isWindows() { 36 | return process.platform === 'win32' 37 | } 38 | 39 | static get isLinux() { 40 | return process.platform === 'linux' 41 | } 42 | 43 | static get isMainProcess() { 44 | return process.type === 'browser' 45 | } 46 | 47 | static get isRendererProcess() { 48 | return process.type === 'renderer' 49 | } 50 | 51 | static get developmentURL() { 52 | const PROTOCOL = process.env.HTTPS === 'true' ? 'https' : 'http' 53 | const PORT = parseInt(process.env.PORT || '', 10) || 3000 54 | const HOST = process.env.HOST || '127.0.0.1' 55 | return `${PROTOCOL}://${HOST}:${PORT}` 56 | } 57 | } 58 | 59 | /** 60 | * Javascript utility class for common or missing built-in operations. 61 | * 62 | * @class JS 63 | */ 64 | export class JS { 65 | static insert(array, index, ...newItems) { 66 | return [ 67 | // Part of the Array before the specified index. 68 | ...array.slice(0, index), 69 | // Inserted items. 70 | ...newItems, 71 | // Part of the Array after the specified index. 72 | ...array.slice(index) 73 | ] 74 | } 75 | 76 | /** 77 | * Deep diff between two objects, returning missing objects as dot notation. 78 | * @param {Object} base Object compared 79 | * @param {Object} object Object to compare with 80 | * @returns {Array} Missing objects as strings in dot notation. 81 | */ 82 | static compareJSON = (base, object) => { 83 | const result = [] 84 | 85 | // eslint-disable-next-line no-shadow 86 | const compare = (base, object, crumbs = '') => { 87 | Object.keys(base).forEach(k => { 88 | switch (typeof base[k]) { 89 | case 'object': 90 | /* Object does not exist anymore in new prefs. */ 91 | if (!Object.prototype.hasOwnProperty.call(object, k)) { 92 | result.push(crumbs ? `${crumbs}.${k}` : k) 93 | } else if (base[k]) { 94 | /* Repeat for nested objects. */ 95 | compare(base[k], object[k], crumbs ? `${crumbs}.${k}` : k) 96 | } 97 | break 98 | default: 99 | /* Pref does not exist anymore in new preferences. */ 100 | if (!Object.keys(object).includes(k)) { 101 | result.push(`${crumbs}.${k}`) 102 | } 103 | } 104 | }) 105 | } 106 | 107 | compare(base, object) 108 | return result 109 | } 110 | 111 | /** 112 | * Deep diff between two objects using lodash 113 | * @param {Object} object Object compared 114 | * @param {Object} base Object to compare with 115 | * @param {Function} customizer Optional customizer function 116 | * @return {Object} Return a new object who represent the diff 117 | */ 118 | static difference(object, base, customizer = null) { 119 | return transform(object, (result, value, key) => { 120 | if (!isEqualWith(value, base[key], customizer || {})) { 121 | // eslint-disable-next-line no-param-reassign 122 | result[key] = 123 | isObject(value) && isObject(base[key]) 124 | ? JS.difference(value, base[key], customizer) 125 | : value 126 | } 127 | }) 128 | } 129 | 130 | // eslint-disable-next-line consistent-return 131 | static customizer(baseValue, value) { 132 | if (Array.isArray(baseValue) && Array.isArray(value)) { 133 | return isEqual(baseValue.sort(), value.sort()) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/shared/docker.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import { chain } from 'lodash' 3 | 4 | export const formatBytes = (bytes, decimals = 2) => { 5 | if (bytes === 0) { 6 | return '0 Bytes' 7 | } 8 | const k = 1024 9 | const dm = decimals <= 0 ? 0 : decimals 10 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 11 | const i = Math.floor(Math.log(bytes) / Math.log(k)) 12 | 13 | return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}` 14 | } 15 | 16 | export const mapImages = images => { 17 | let key = -1 18 | return images.map(image => { 19 | key += 1 20 | return { 21 | id: key, 22 | _id: image.Id.slice(7, 15), // sha256: 23 | size: image.Size, 24 | date: new Date(image.Created * 1000), 25 | containers: image.Containers === -1 ? 0 : image.Containers, 26 | tags: image.RepoTags === null ? ['N/A'] : image.RepoTags 27 | } 28 | }) 29 | } 30 | 31 | export const mapContainers = containers => { 32 | let key = -1 33 | return containers.map(container => { 34 | key += 1 35 | return { 36 | id: key, 37 | _id: container.Id.slice(0, 8), 38 | name: chain(container.Names) 39 | .map(n => n.substr(1)) 40 | .join(', ') 41 | .value(), 42 | state: container.State, 43 | status: container.Status, 44 | image: container.Image 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/shared/file.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | 6 | import electron from 'electron' 7 | 8 | class ElectronFileHandler { 9 | constructor(opts) { 10 | const userDataPath = (electron.app || electron.remote.app).getPath('userData') 11 | 12 | this.path = path.join(userDataPath, opts.configName) 13 | this.data = opts.defaults 14 | } 15 | 16 | get(key) { 17 | return this.data[key] 18 | } 19 | 20 | set(key, value) { 21 | this.data[key] = value 22 | } 23 | 24 | exists = () => { 25 | return fs.existsSync(this.path) 26 | } 27 | 28 | saveFile = () => { 29 | fs.writeFileSync(this.path, JSON.stringify(this.data)) 30 | } 31 | 32 | readFile = () => { 33 | this.data = JSON.parse(fs.readFileSync(this.path)) 34 | } 35 | } 36 | 37 | export default ElectronFileHandler 38 | -------------------------------------------------------------------------------- /src/shared/fuzzmanager.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import fs from 'fs' 3 | 4 | import ini from 'ini' 5 | import ElectronFileHandler from './file' 6 | 7 | class FuzzManagerConf extends ElectronFileHandler { 8 | saveFile = () => { 9 | fs.writeFileSync(this.path, ini.stringify(this.data, { section: 'Main' })) 10 | } 11 | 12 | readFile = () => { 13 | this.data = ini.parse(fs.readFileSync(this.path, 'utf-8')) 14 | this.data = this.data.Main 15 | return this.data 16 | } 17 | } 18 | 19 | export default FuzzManagerConf 20 | -------------------------------------------------------------------------------- /src/shared/logger.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import log from 'electron-log' 4 | 5 | import { Package } from './common' 6 | 7 | /** 8 | * Logger that logs to file and to console. 9 | * 10 | * Locations: 11 | * Linux: `~/.config//$logName.log` 12 | * MacOS: `~/Library/Logs//$logName.log` 13 | * Windows: `%USERPROFILE%\AppData\Roaming\\$logName.log` 14 | * 15 | * @class Logger 16 | */ 17 | class Logger { 18 | /** 19 | * Logger constructor. 20 | * 21 | * @param moduleName {string} Module name of caller. 22 | * @param level {Number} Log level number. 23 | * @param logName {string} Log file name. 24 | */ 25 | constructor(moduleName = 'root', level = 'info', logName = `${Package.name}.log`) { 26 | this.moduleName = moduleName 27 | 28 | this.setLogname(logName) 29 | this.setLevel(level) 30 | } 31 | 32 | /** 33 | * Log error 34 | * 35 | * @param args {any} Data that needs to be logged. 36 | */ 37 | error(...args) { 38 | return log.error(`[${this.moduleName}]`, ...args) 39 | } 40 | 41 | /** 42 | * Log warn 43 | * 44 | * @param args {any} Data that needs to be logged. 45 | */ 46 | warning(...args) { 47 | return log.warn(`[${this.moduleName}]`, ...args) 48 | } 49 | 50 | /** 51 | * Log info 52 | * 53 | * @param args {any} Data that needs to be logged. 54 | */ 55 | info(...args) { 56 | return log.info(`[${this.moduleName}]`, ...args) 57 | } 58 | 59 | /** 60 | * Log verbose 61 | * 62 | * @param args {any} Data that needs to be logged. 63 | */ 64 | verbose(...args) { 65 | return log.verbose(`[${this.moduleName}]`, ...args) 66 | } 67 | 68 | /** 69 | * Log debug 70 | * 71 | * @param args {any} Data that needs to be logged. 72 | */ 73 | debug(...args) { 74 | return log.debug(`[${this.moduleName}]`, ...args) 75 | } 76 | 77 | /** 78 | * Log name 79 | * 80 | * @param name {string} Filename of the logfile. 81 | */ 82 | // eslint-disable-next-line class-methods-use-this 83 | setLogname(name) { 84 | log.transports.file.fileName = name 85 | } 86 | 87 | /** 88 | * Log level 89 | * 90 | * @param level {number} Log level number. 91 | */ 92 | // eslint-disable-next-line class-methods-use-this 93 | setLevel(level) { 94 | log.transports.file.level = level 95 | log.transports.console.level = level 96 | } 97 | } 98 | 99 | export default Logger 100 | -------------------------------------------------------------------------------- /src/shared/monitor.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import http from 'http' 4 | import https from 'https' 5 | import { promises as DNS } from 'dns' 6 | 7 | /** 8 | * 9 | * @param host 10 | * @returns {Promise} 11 | */ 12 | export const isOnline = (host = 'mozilla.org') => { 13 | return DNS.lookup(host) 14 | } 15 | 16 | /** 17 | * 18 | * @param url 19 | * @param allowStatus 20 | * @returns {Promise} 21 | */ 22 | export const isReachable = (url, allowStatus = [302]) => { 23 | return new Promise((resolve, reject) => { 24 | return https.get(url, response => { 25 | if (response.statusCode >= 200 && response.statusCode <= 300) { 26 | resolve({ code: response.statusCode }) 27 | } 28 | if (allowStatus.includes(parseInt(response.statusCode, 10))) { 29 | resolve({ code: response.statusCode }) 30 | } 31 | // eslint-disable-next-line prefer-promise-reject-errors 32 | reject({ code: response.statusCode, message: http.STATUS_CODES[response.statusCode] }) 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/shared/sentry.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import * as Sentry from '@sentry/electron' 4 | 5 | import Store from '../main/store' 6 | import Logger from './logger' 7 | 8 | const logger = new Logger('Sentry') 9 | 10 | /** 11 | * Sentry initialization for renderer and main process. 12 | */ 13 | logger.info(`Initializing Sentry for process: ${process.type}`) 14 | Sentry.init({ 15 | dsn: Store.get('preferences.sentry.dsn'), 16 | environment: process.env.NODE_ENV, 17 | // eslint-disable-next-line no-unused-vars 18 | onFatalError: error => { 19 | process.exit(1) 20 | } 21 | }) 22 | 23 | export default Sentry 24 | -------------------------------------------------------------------------------- /src/shared/serializers.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class LibFuzzer { 4 | static command = 'cat /home/worker/stats' 5 | 6 | static serialize = data => { 7 | const result = {} 8 | if (typeof data !== 'string') { 9 | return result 10 | } 11 | data.split('\n').forEach(line => { 12 | const pair = line.match(/\w+/g) 13 | if (pair && pair.length === 2) { 14 | result[pair[0]] = pair[1] 15 | } 16 | }) 17 | return result 18 | } 19 | } 20 | 21 | export default { 22 | LibFuzzer: LibFuzzer 23 | } 24 | --------------------------------------------------------------------------------