├── .eslintrc.js ├── .github └── workflows │ └── build-main.yml ├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── assets └── scss │ ├── _login.scss │ ├── _structure.scss │ ├── main.scss │ └── theme.scss ├── babel.config.js ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── apis │ │ ├── auditing.js │ │ ├── auth.js │ │ ├── client.js │ │ ├── dashboard.js │ │ ├── filemanager.js │ │ ├── hashcat.js │ │ ├── index.js │ │ ├── system.js │ │ ├── tasks.js │ │ └── users.js │ ├── config.js │ ├── index.js │ └── types.js ├── components │ ├── CreateTask │ │ ├── AvailableEngineSelector.vue │ │ ├── CaseCodeInput.vue │ │ ├── TaskFileSelector.vue │ │ ├── TaskNameInput.vue │ │ ├── TimeLimitInput.vue │ │ └── index.js │ ├── DeviceSelection.vue │ ├── DisplayFileInfo.vue │ ├── DownloadButton.vue │ ├── Hashcat │ │ ├── AttackModeSelector.vue │ │ ├── HashTypesSelector.vue │ │ └── index.js │ ├── PasswordLengthBarGraph.js │ ├── PasswordTable.vue │ ├── StopTaskModal.vue │ ├── TaskStatusTable.vue │ ├── UpdateTaskModal.vue │ ├── UploadFileModal.vue │ └── UsersSelection.vue ├── config.js ├── faloader │ └── index.js ├── filters │ ├── index.js │ └── numeral.js ├── helpers │ ├── generate_csv.js │ └── index.js ├── i18n │ ├── en-US.json │ └── index.js ├── main.js ├── mixins │ ├── chCompValidator.js │ └── index.js ├── router │ └── index.js ├── store │ ├── helpers.js │ ├── index.js │ ├── modules │ │ ├── authentication.js │ │ ├── browser_notify.js │ │ ├── realtime.js │ │ ├── user_local_settings.js │ │ └── workers.js │ └── mutations.js ├── toast │ ├── README.md │ ├── Toast.vue │ ├── config.js │ ├── index.js │ ├── module.js │ └── utils.js ├── validators │ ├── device_selection.js │ └── index.js └── views │ ├── CreateTask.vue │ ├── Dashboard.vue │ ├── ErrorServer.vue │ ├── ErrorUnauthorized.vue │ ├── ListFiles.vue │ ├── ListTask.vue │ ├── Login.vue │ ├── ModifyUser.vue │ ├── RegisterUser.vue │ ├── TaskStatus.vue │ ├── UserListing.vue │ └── VersionInfo.vue ├── static └── img │ └── logo.png ├── vue.config.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | }, 17 | plugins: [ 18 | '@vue' 19 | ], 20 | 'globals': { 21 | '$': true, 22 | 'jQuery': true, 23 | 'VERSION': true, 24 | 'COMMITHASH': true, 25 | 'BRANCH': true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/build-main.yml: -------------------------------------------------------------------------------- 1 | name: Build master 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Setup Node 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 12 17 | 18 | - name: Install dependencies 19 | run: yarn install 20 | 21 | - name: Build UI 22 | run: yarn build 23 | 24 | - name: Upload release package 25 | uses: actions/upload-artifact@v3 26 | with: 27 | name: dist 28 | path: dist 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Christopher Schmitt, FireEye 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILDDATE := $(shell date +%s) 2 | BUILDREV := $(shell git log --pretty=format:'%h' -n 1) 3 | 4 | pack: 5 | npm run build 6 | tar --exclude=*.map -czvf gocrack-ui-$(BUILDREV)-$(BUILDDATE).tgz dist/* 7 | 8 | clean: 9 | rm -rf dist/ 10 | rm *.tgz 11 | 12 | all: pack -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoCrack UI 2 | 3 | Welcome! This includes the UI related code for [GoCrack](https://github.com/fireeye/gocrack). 4 | 5 | ## Project setup 6 | ``` 7 | yarn install 8 | ``` 9 | 10 | ### Compiles and hot-reloads for development 11 | ``` 12 | yarn run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | ``` 17 | yarn run build 18 | ``` 19 | 20 | ### Run your tests 21 | ``` 22 | yarn run test 23 | ``` 24 | 25 | ### Lints and fixes files 26 | ``` 27 | yarn run lint 28 | ``` 29 | 30 | ### Customize configuration 31 | See [Configuration Reference](https://cli.vuejs.org/config/). 32 | -------------------------------------------------------------------------------- /assets/scss/_login.scss: -------------------------------------------------------------------------------- 1 | .form-signin { 2 | max-width: 330px; 3 | padding: 15px; 4 | margin: 0 auto; 5 | } 6 | 7 | .form-signin .form-signin-heading, 8 | .form-signin .checkbox { 9 | margin-bottom: 10px; 10 | } 11 | 12 | .form-signin .checkbox { 13 | font-weight: normal; 14 | } 15 | 16 | .form-signin .form-control { 17 | position: relative; 18 | height: auto; 19 | -webkit-box-sizing: border-box; 20 | box-sizing: border-box; 21 | padding: 10px; 22 | font-size: 16px; 23 | } 24 | 25 | .form-signin .form-control:focus { 26 | z-index: 2; 27 | } 28 | 29 | .form-signin input[type="username"] { 30 | margin-bottom: -1px; 31 | border-bottom-right-radius: 0; 32 | border-bottom-left-radius: 0; 33 | } 34 | 35 | .form-signin input[type="password"] { 36 | margin-bottom: 10px; 37 | border-top-left-radius: 0; 38 | border-top-right-radius: 0; 39 | } 40 | 41 | form.form-signin a { 42 | margin: 5px 0px; 43 | } 44 | 45 | form.form-signin a:hover, a:focus { 46 | text-decoration: none !important; 47 | } 48 | -------------------------------------------------------------------------------- /assets/scss/_structure.scss: -------------------------------------------------------------------------------- 1 | // Contains Style Information that is important for the structure of the application 2 | html { 3 | position: relative; 4 | min-height: 100%; 5 | } 6 | 7 | body { 8 | margin-bottom: 100px; 9 | } 10 | 11 | .container-fluid { 12 | padding-top: 10px; 13 | padding-bottom: 50px; 14 | } 15 | 16 | .container { 17 | width: auto; 18 | padding: 0 15px; 19 | } 20 | 21 | footer.footer { 22 | background-color: #f5f5f5; 23 | position: absolute; 24 | bottom: 0; 25 | width: 100%; 26 | height: 60px; 27 | overflow: hidden; 28 | } 29 | 30 | ul.no-style { 31 | list-style-type: none; 32 | } 33 | 34 | .noflow { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | word-wrap: break-word; 38 | } 39 | 40 | .form-feedback { 41 | display: block; 42 | } -------------------------------------------------------------------------------- /assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "theme.scss"; 2 | 3 | @import 'node_modules/bootstrap/scss/bootstrap'; 4 | @import 'node_modules/bootstrap-vue/src/index.scss'; 5 | 6 | @import "_structure.scss"; 7 | @import "_login.scss"; 8 | -------------------------------------------------------------------------------- /assets/scss/theme.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --theme-dark-bg: #343a40; 3 | --theme-border-color: #dee2e6; 4 | --theme-bg-color: #fff; 5 | --theme-body-color: #212529; 6 | --theme-card-bg: #fff; 7 | --theme-card-border-color: rgba(00, 00, 00, .125); 8 | --theme-card-cap-bg: rgba(00, 00, 00, .03); 9 | --theme-hr-border-color: rgba(00, 00, 00, .1); 10 | --theme-input-bg: #fff; 11 | --theme-input-border-color: #ced4da; 12 | --theme-input-color: #495057; 13 | --theme-input-disabled-bg: #e9ecef; 14 | --theme-input-group-addon-bg: #e9ecef; 15 | --theme-dropdown-bg: #fff; 16 | --theme-dropdown-link-color: #212529; 17 | --theme-table-accent-bg: rgba(00, 00, 00, .05); 18 | } 19 | 20 | :root[data-theme="dark"] { 21 | --theme-dark-bg: rgba(00, 00, 00, .09); 22 | --theme-border-color: #343a40; 23 | --theme-bg-color: #212529; 24 | --theme-body-color: #fff; 25 | --theme-card-bg: #212529; 26 | --theme-card-border-color: rgba(00, 00, 00, .5); 27 | --theme-card-cap-bg: rgba(00, 00, 00, .12); 28 | --theme-hr-border-color: rgba(00, 00, 00, .4); 29 | --theme-input-bg: #343a40; 30 | --theme-input-border-color: rgba(00, 00, 00, .4); 31 | --theme-input-color: #f8f9fa; 32 | --theme-input-disabled-bg: #212529; 33 | --theme-input-group-addon-bg: #111215; 34 | --theme-dropdown-bg: #343a40; 35 | --theme-dropdown-link-color: #fff; 36 | --theme-table-accent-bg: rgba(00, 00, 00, .2); 37 | } 38 | 39 | // Body 40 | $body-bg: var(--theme-bg-color); 41 | $body-color: var(--theme-body-color); 42 | 43 | // Components 44 | // 45 | // Define common padding and border radius sizes and more. 46 | $border-color: var(--theme-border-color); 47 | 48 | // Cards 49 | $card-bg: var(--theme-card-bg); 50 | $card-border-color: var(--theme-card-border-color); 51 | $card-cap-bg: var(--theme-card-cap-bg); 52 | 53 | // Typography 54 | $hr-border-color: var(--theme-hr-border-color); 55 | 56 | // Forms 57 | $input-bg: var(--theme-input-bg); 58 | $input-border-color: var(--theme-input-border-color); 59 | $input-disabled-bg: var(--theme-input-disabled-bg); 60 | $input-color: var(--theme-input-color); 61 | $input-group-addon-bg: var(--theme-input-group-addon-bg); 62 | 63 | // Dropdowns 64 | // 65 | // Dropdown menu container and contents. 66 | $dropdown-bg: var(--theme-dropdown-bg); 67 | $dropdown-link-color: var(--theme-dropdown-link-color); 68 | 69 | // Tables 70 | // 71 | // Customizes the `.table` component with basic values, each used across all table variations. 72 | $table-accent-bg: var(--theme-table-accent-bg); 73 | 74 | // Modals 75 | $modal-content-bg: var(--theme-bg-color); 76 | 77 | .theme-dark { 78 | background-color: var(--theme-dark-bg); 79 | } 80 | 81 | .multiselect { 82 | .multiselect__single { 83 | background: var(--theme-input-bg); 84 | border-color: var(--theme-input-border-color); 85 | color: var(--theme-input-color); 86 | } 87 | 88 | .multiselect__tags { 89 | background: var(--theme-input-bg); 90 | border-color: var(--theme-input-border-color); 91 | color: var(--theme-input-color); 92 | } 93 | 94 | .multiselect__input { 95 | background: var(--theme-input-bg); 96 | border-color: var(--theme-input-border-color); 97 | color: var(--theme-input-color); 98 | } 99 | 100 | .multiselect__content-wrapper { 101 | background: var(--theme-input-bg); 102 | border-color: var(--theme-input-border-color); 103 | color: var(--theme-input-color); 104 | } 105 | 106 | .multiselect__spinner { 107 | background: var(--theme-input-bg); 108 | } 109 | } 110 | 111 | .page-link { 112 | background: var(--theme-input-bg) !important; 113 | } 114 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gocrack-ui", 3 | "version": "1.2.0", 4 | "description": "Web UI for GoCrack", 5 | "author": "Christopher Schmitt ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/fireeye/gocrack-ui" 10 | }, 11 | "scripts": { 12 | "serve": "export VUE_APP_VERSION=$(git rev-parse HEAD) && vue-cli-service serve", 13 | "build": "export VUE_APP_VERSION=$(git rev-parse HEAD) && vue-cli-service build", 14 | "lint": "vue-cli-service lint" 15 | }, 16 | "dependencies": { 17 | "@fortawesome/fontawesome-svg-core": "^1.2.19", 18 | "@fortawesome/free-solid-svg-icons": "^5.9.0", 19 | "@fortawesome/vue-fontawesome": "^0.1.6", 20 | "@sentry/browser": "^5.4.0", 21 | "@sentry/integrations": "^5.4.1", 22 | "axios": "^0.19.0", 23 | "bootstrap-vue": "^2.0.0-rc.22", 24 | "chart.js": "^2.8.0", 25 | "core-js": "^2.6.5", 26 | "jquery": "^3.4.1", 27 | "jwt-decode": "^2.2.0", 28 | "node-sass": "^4.12.0", 29 | "numeral": "^2.0.6", 30 | "sass-loader": "^7.1.0", 31 | "vue": "^2.6.10", 32 | "vue-chartjs": "^3.4.2", 33 | "vue-i18n": "^8.11.2", 34 | "vue-multiselect": "^2.1.6", 35 | "vue-router": "^3.0.6", 36 | "vue-tables-2": "^1.4.70", 37 | "vuelidate": "^0.7.4", 38 | "vuex": "^3.1.1", 39 | "vuex-router-sync": "^5.0.0" 40 | }, 41 | "devDependencies": { 42 | "@fortawesome/fontawesome-free": "^5.9.0", 43 | "@vue/cli-plugin-babel": "^3.8.0", 44 | "@vue/cli-plugin-eslint": "^3.8.0", 45 | "@vue/cli-service": "^3.8.0", 46 | "@vue/eslint-config-standard": "^4.0.0", 47 | "@vue/eslint-plugin": "^4.2.0", 48 | "babel-eslint": "^10.0.1", 49 | "eslint": "^5.16.0", 50 | "eslint-config-standard": "^12.0.0", 51 | "eslint-loader": "^2.1.2", 52 | "eslint-plugin-html": "^5.0.5", 53 | "eslint-plugin-import": "^2.17.3", 54 | "eslint-plugin-node": "^9.1.0", 55 | "eslint-plugin-promise": "^4.1.1", 56 | "eslint-plugin-standard": "^4.0.0", 57 | "eslint-plugin-vue": "^5.0.0", 58 | "vue-template-compiler": "^2.6.10", 59 | "webpack": "^4.33.0" 60 | }, 61 | "babel": { 62 | "presets": [ 63 | "@vue/app" 64 | ] 65 | }, 66 | "eslintConfig": { 67 | "root": true, 68 | "env": { 69 | "node": true 70 | }, 71 | "extends": [ 72 | "plugin:vue/essential", 73 | "eslint:recommended" 74 | ], 75 | "rules": {}, 76 | "parserOptions": { 77 | "parser": "babel-eslint" 78 | } 79 | }, 80 | "postcss": { 81 | "plugins": { 82 | "autoprefixer": {} 83 | } 84 | }, 85 | "browserslist": [ 86 | "> 1%", 87 | "last 2 versions" 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/gocrack-ui/a6f0b8733ab7b2a60c24052dc690d568abbc74b5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | GoCrack 9 | 10 | 11 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 62 | 63 | 125 | -------------------------------------------------------------------------------- /src/api/apis/auditing.js: -------------------------------------------------------------------------------- 1 | import client from './client' 2 | 3 | export default { 4 | getAuditLog (apiConfig, entityID) { 5 | return client.performRequest(apiConfig, 'GET', `/audit/${entityID}`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/apis/auth.js: -------------------------------------------------------------------------------- 1 | import client from './client' 2 | 3 | export default { 4 | loginUser (apiConfig, loginData) { 5 | if (loginData === undefined || typeof (loginData) !== 'object') { 6 | throw Error('loginData must be an object') 7 | } 8 | return client.performRequest(apiConfig, 'POST', '/login', { data: loginData }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/api/apis/client.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const debug = process.env.NODE_ENV !== 'production' 4 | 5 | export default { 6 | performRequest (apiConfig, method, route, optionalAxiosOptions = {}) { 7 | let axiosOptions = { 8 | method: method, 9 | withCredentials: true, 10 | xsrfHeaderName: 'X-Xsrf-Token', 11 | xsrfCookieName: 'XSRF-TOKEN', 12 | baseURL: `${apiConfig.server}${apiConfig.base_endpoint}/`, 13 | url: route, 14 | ...optionalAxiosOptions 15 | } 16 | 17 | if (debug) { 18 | console.log(`Making ${axiosOptions.method} request to server ${axiosOptions.baseURL} to path ${axiosOptions.url}`) 19 | } 20 | 21 | return axios.request(axiosOptions).then((resp) => { 22 | if (axiosOptions.method === 'POST' || axiosOptions.method === 'PUT' || axiosOptions.method === 'PATCH') { 23 | // No content 24 | if (resp.status === 204) { 25 | return true 26 | } 27 | } 28 | 29 | return resp.data 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/api/apis/dashboard.js: -------------------------------------------------------------------------------- 1 | import client from './client' 2 | 3 | export default { 4 | getWorkers (apiConfig) { 5 | return client.performRequest(apiConfig, 'GET', '/workers/') 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/apis/filemanager.js: -------------------------------------------------------------------------------- 1 | import client from './client' 2 | import apitypes from '../types' 3 | 4 | export default { 5 | getTaskFiles (apiConfig, entityID) { 6 | return client.performRequest(apiConfig, 'GET', `/files/task/`) 7 | }, 8 | 9 | getEngineFiles (apiConfig) { 10 | return client.performRequest(apiConfig, 'GET', `/files/engine/`) 11 | }, 12 | 13 | uploadFile (apiConfig, file, fileType, params, progressFunc = undefined) { 14 | let url 15 | switch (fileType) { 16 | case apitypes.FILE_ENGINE: 17 | url = `/files/engine/${file.name}` 18 | break 19 | case apitypes.FILE_TASK: 20 | url = `/files/task/${file.name}` 21 | break 22 | default: 23 | throw Error('fileType must be FILE_ENGINE or FILE_TASK') 24 | } 25 | 26 | let data = new FormData() 27 | data.append('file', file) 28 | 29 | let axiosconfig = { params: params, data: data } 30 | if (progressFunc !== undefined) { 31 | axiosconfig.onUploadProgress = progressFunc 32 | } 33 | return client.performRequest(apiConfig, 'PUT', url, axiosconfig) 34 | }, 35 | 36 | downloadFile (apiConfig, fileID, fileType) { 37 | let url 38 | switch (fileType) { 39 | case apitypes.FILE_ENGINE: 40 | url = `/files/engine/${fileID}/download` 41 | break 42 | case apitypes.FILE_TASK: 43 | url = `/files/task/${fileID}/download` 44 | break 45 | default: 46 | throw Error('fileType must be FILE_ENGINE or FILE_TASK') 47 | } 48 | 49 | return client.performRequest(apiConfig, 'GET', url) 50 | }, 51 | 52 | deleteFile (apiConfig, fileID, fileType) { 53 | let url 54 | switch (fileType) { 55 | case apitypes.FILE_ENGINE: 56 | url = `/files/engine/${fileID}` 57 | break 58 | case apitypes.FILE_TASK: 59 | url = `/files/task/${fileID}` 60 | break 61 | default: 62 | throw Error('fileType must be FILE_ENGINE or FILE_TASK') 63 | } 64 | 65 | return client.performRequest(apiConfig, 'DELETE', url) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/api/apis/hashcat.js: -------------------------------------------------------------------------------- 1 | import client from './client' 2 | 3 | export default { 4 | getHashcatTypes (apiConfig) { 5 | return client.performRequest(apiConfig, 'GET', '/engine/hashcat/hash_modes') 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/apis/index.js: -------------------------------------------------------------------------------- 1 | import auth from './auth' 2 | import dashboard from './dashboard' 3 | import tasks from './tasks' 4 | import auditing from './auditing' 5 | import hashcat from './hashcat' 6 | import users from './users' 7 | import filemanager from './filemanager' 8 | import system from './system' 9 | 10 | var wrapConfig = function (fn, cfg) { 11 | return function () { 12 | return fn.apply(this, [cfg, ...arguments]) 13 | } 14 | } 15 | 16 | export default { 17 | wrapAPIWithConfig (config) { 18 | return { 19 | // ./system 20 | getVersionInfo: wrapConfig(system.getVersionInfo, config), 21 | // ./auth 22 | login: wrapConfig(auth.loginUser, config), 23 | // ./dashboard 24 | getWorkers: wrapConfig(dashboard.getWorkers, config), 25 | // ./tasks 26 | getTasks: wrapConfig(tasks.getTasks, config), 27 | getTaskInfo: wrapConfig(tasks.getTaskInfo, config), 28 | modifyTask: wrapConfig(tasks.modifyTask, config), 29 | getPasswords: wrapConfig(tasks.getCrackedPasswords, config), 30 | createTask: wrapConfig(tasks.createTask, config), 31 | modifyTaskStatus: wrapConfig(tasks.modifyTaskStatus, config), 32 | deleteTask: wrapConfig(tasks.deleteTask, config), 33 | // ./auditing 34 | getAuditLog: wrapConfig(auditing.getAuditLog, config), 35 | // ./hashcat 36 | getHashcatTypes: wrapConfig(hashcat.getHashcatTypes, config), 37 | // ./users 38 | getUsers: wrapConfig(users.getUsers, config), 39 | getUserInfo: wrapConfig(users.getUserInfo, config), 40 | modifyUserInfo: wrapConfig(users.modifyUserInfo, config), 41 | createUser: wrapConfig(users.createUser, config), 42 | // ./filemanager 43 | getTaskFiles: wrapConfig(filemanager.getTaskFiles, config), 44 | getEngineFiles: wrapConfig(filemanager.getEngineFiles, config), 45 | uploadFile: wrapConfig(filemanager.uploadFile, config), 46 | downloadFile: wrapConfig(filemanager.downloadFile, config), 47 | deleteFile: wrapConfig(filemanager.deleteFile, config) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/api/apis/system.js: -------------------------------------------------------------------------------- 1 | import client from './client' 2 | 3 | export default { 4 | getVersionInfo (apiConfig) { 5 | return client.performRequest(apiConfig, 'GET', `/version/`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/apis/tasks.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import client from './client' 3 | import apitypes from '../types' 4 | 5 | export default { 6 | getTasks (apiConfig, data) { 7 | // We're not using the helper here as the table component expects the raw axios response 8 | return axios.request({ 9 | method: 'GET', 10 | xsrfHeaderName: 'X-Xsrf-Token', 11 | xsrfCookieName: 'XSRF-TOKEN', 12 | baseURL: `${apiConfig.server}${apiConfig.base_endpoint}/`, 13 | url: '/task/', 14 | params: data, 15 | withCredentials: true 16 | }) 17 | }, 18 | 19 | getTaskInfo (apiConfig, taskId) { 20 | return client.performRequest(apiConfig, 'GET', `/task/${taskId}`) 21 | }, 22 | 23 | getCrackedPasswords (apiConfig, taskId) { 24 | return client.performRequest(apiConfig, 'GET', `/task/${taskId}/passwords`).then((response) => { 25 | let { data, count } = response 26 | if (count > 0) { 27 | data = data.map(el => { 28 | return { 29 | ...el, 30 | cracked_at: new Date(el.cracked_at).toUTCString() 31 | } 32 | }) 33 | } 34 | return data 35 | }) 36 | }, 37 | 38 | modifyTask (apiConfig, taskId, payload) { 39 | return client.performRequest(apiConfig, 'PATCH', `/task/${taskId}`, { data: payload }) 40 | }, 41 | 42 | createTask (apiConfig, payload) { 43 | return client.performRequest(apiConfig, 'POST', `/task/`, { data: payload }) 44 | }, 45 | 46 | modifyTaskStatus (apiConfig, taskId, action) { 47 | console.log(action === apitypes.TASK_START) 48 | if (action === apitypes.TASK_START || action === apitypes.TASK_STOP) { 49 | return client.performRequest(apiConfig, 'PATCH', `/task/${taskId}/status`, { data: { state: action } }) 50 | } 51 | throw Error('incorrect action. must be TASK_START or TASK_STOP') 52 | }, 53 | 54 | deleteTask (apiConfig, taskId) { 55 | return client.performRequest(apiConfig, 'DELETE', `/task/${taskId}`) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/api/apis/users.js: -------------------------------------------------------------------------------- 1 | import client from './client' 2 | 3 | export default { 4 | getUsers (apiConfig) { 5 | return client.performRequest(apiConfig, 'GET', `/users/`) 6 | }, 7 | 8 | getUserInfo (apiConfig, userId) { 9 | return client.performRequest(apiConfig, 'GET', `/users/${userId}`) 10 | }, 11 | 12 | modifyUserInfo (apiConfig, userId, payload) { 13 | return client.performRequest(apiConfig, 'PATCH', `/users/${userId}`, { data: payload }) 14 | }, 15 | 16 | createUser (apiConfig, payload) { 17 | return client.performRequest(apiConfig, 'POST', `/users/register`, { data: payload }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/api/config.js: -------------------------------------------------------------------------------- 1 | const _config = {} 2 | 3 | const GoCrackConfig = { 4 | set: config => Object.assign(_config, config), 5 | get: _config 6 | } 7 | 8 | Object.freeze(GoCrackConfig) 9 | 10 | export default GoCrackConfig 11 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import apis from './apis' 2 | import GoCrackConfig from './config' 3 | 4 | const VuePlugin = { 5 | install: function (Vue, config) { 6 | if (Vue._gocrack_api_installed) { 7 | return 8 | } 9 | 10 | if (config.server === undefined) { 11 | config.server = '' 12 | } 13 | 14 | Vue._gocrack_api_installed = true 15 | GoCrackConfig.set(config) 16 | 17 | Vue.mixin({ 18 | created () { 19 | this.$config = GoCrackConfig 20 | this.$gocrack = apis.wrapAPIWithConfig(config) 21 | } 22 | }) 23 | } 24 | } 25 | 26 | export default VuePlugin 27 | -------------------------------------------------------------------------------- /src/api/types.js: -------------------------------------------------------------------------------- 1 | 2 | // these should mirror shared/hashcat_types.go based on HashcatAttackMode 3 | const HC_STRAIGHT = 0 4 | const HC_BF = 3 5 | const ENG_HC = 1 6 | 7 | export default { 8 | // server/storage/schemas.go based on WorkerCrackEngine 9 | ENGINE_HASHCAT: ENG_HC, 10 | 11 | FILE_ENGINE: 0, 12 | FILE_TASK: 1, 13 | 14 | TASK_START: 'start', 15 | TASK_STOP: 'stop', 16 | 17 | HASHCAT_ATTACKMODE_STRAIGHT: HC_STRAIGHT, 18 | HASHCAT_ATTACKMODE_COMBINATION: 1, 19 | HASHCAT_ATTACKMODE_BF: HC_BF, 20 | HASHCAT_ATTACK_MODES: [ 21 | { 22 | name: 'Dictionary/Straight', 23 | id: HC_STRAIGHT, 24 | default: true 25 | }, 26 | { 27 | name: 'Brute Force', 28 | id: HC_BF 29 | } 30 | ], 31 | GOCRACK_ENGINES: [ 32 | { 33 | name: 'Hashcat', 34 | id: ENG_HC 35 | } 36 | ], 37 | ENGINE_FILE_TYPES: [ 38 | { 39 | name: 'Dictionary', 40 | id: 0 41 | }, 42 | { 43 | name: 'Brute Force Mask(s)', 44 | id: 1 45 | }, 46 | { 47 | name: 'Mangling Rule(s)', 48 | id: 2 49 | } 50 | ], 51 | TASK_FILE_FOR_ENGINE: [ 52 | { 53 | name: 'All', 54 | id: 0 55 | }, 56 | { 57 | name: 'Hashcat', 58 | id: ENG_HC 59 | } 60 | ], 61 | TASK_STATUSES: ['Queued', 'Running', 'Stopped', 'Error', 'Exhausted', 'Finished'], 62 | TASK_PRIORITIES: [ 63 | { text: 'High (up to 4 GPUs)', value: 0 }, 64 | { text: 'Normal (up to 2 GPUs)', value: 1 }, 65 | { text: 'Low (1 GPU)', value: 2 } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/components/CreateTask/AvailableEngineSelector.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 47 | -------------------------------------------------------------------------------- /src/components/CreateTask/CaseCodeInput.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | -------------------------------------------------------------------------------- /src/components/CreateTask/TaskFileSelector.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 81 | -------------------------------------------------------------------------------- /src/components/CreateTask/TaskNameInput.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /src/components/CreateTask/TimeLimitInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | -------------------------------------------------------------------------------- /src/components/CreateTask/index.js: -------------------------------------------------------------------------------- 1 | import AvailableEngineSelector from './AvailableEngineSelector' 2 | import TimeLimitInput from './TimeLimitInput' 3 | import CaseCodeInput from './CaseCodeInput' 4 | import TaskNameInput from './TaskNameInput' 5 | import TaskFileSelector from './TaskFileSelector' 6 | 7 | export { 8 | AvailableEngineSelector, 9 | TimeLimitInput, 10 | CaseCodeInput, 11 | TaskNameInput, 12 | TaskFileSelector 13 | } 14 | -------------------------------------------------------------------------------- /src/components/DeviceSelection.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 87 | -------------------------------------------------------------------------------- /src/components/DisplayFileInfo.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 93 | -------------------------------------------------------------------------------- /src/components/DownloadButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 54 | -------------------------------------------------------------------------------- /src/components/Hashcat/AttackModeSelector.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 52 | -------------------------------------------------------------------------------- /src/components/Hashcat/HashTypesSelector.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 76 | -------------------------------------------------------------------------------- /src/components/Hashcat/index.js: -------------------------------------------------------------------------------- 1 | import HashcatAttackModeSelector from './AttackModeSelector' 2 | import HashcatHashTypesSelector from './HashTypesSelector' 3 | 4 | export { 5 | HashcatAttackModeSelector, 6 | HashcatHashTypesSelector 7 | } 8 | -------------------------------------------------------------------------------- /src/components/PasswordLengthBarGraph.js: -------------------------------------------------------------------------------- 1 | 2 | import { Bar } from 'vue-chartjs' 3 | 4 | export default Bar.extend({ 5 | props: ['options'], 6 | mounted () { 7 | this.$parent.$on('passwordLengthStatsGenerated', (stats) => { 8 | this.renderChart(stats, this.options || { 9 | responsive: true, 10 | maintainAspectRatio: true, 11 | display: true, 12 | legend: { 13 | display: true 14 | }, 15 | scales: { 16 | xAxes: [ 17 | { 18 | scaleLabel: { 19 | display: true, 20 | labelString: this.$t('graph.password_lengths') 21 | } 22 | } 23 | ], 24 | yAxes: [ 25 | { 26 | scaleLabel: { 27 | display: true, 28 | labelString: this.$t('graph.num_cracked_passwords') 29 | }, 30 | gridLines: { 31 | display: true 32 | } 33 | } 34 | ] 35 | } 36 | }) 37 | }) 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /src/components/PasswordTable.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 | 34 | 156 | -------------------------------------------------------------------------------- /src/components/StopTaskModal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 46 | -------------------------------------------------------------------------------- /src/components/TaskStatusTable.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 106 | 107 | 135 | -------------------------------------------------------------------------------- /src/components/UpdateTaskModal.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 149 | -------------------------------------------------------------------------------- /src/components/UploadFileModal.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 273 | -------------------------------------------------------------------------------- /src/components/UsersSelection.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 73 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | export const APP_VERSION = process.env.VUE_APP_VERSION 4 | export const SENTRY_DSN = process.env.VUE_APP_GOCRACK_SENTRY_DSN 5 | 6 | export default { 7 | APP_VERSION, 8 | SENTRY_DSN 9 | } 10 | -------------------------------------------------------------------------------- /src/faloader/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { library, dom } from '@fortawesome/fontawesome-svg-core' 3 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 4 | 5 | import { faUserCircle, faEdit, faSearch, faLifeRing, faSort, faArrowUp, faArrowDown, faChevronLeft, faChevronRight, faSortAlphaDown, faSortAlphaUp, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' 6 | 7 | library.add( 8 | faUserCircle, 9 | faEdit, 10 | faSearch, 11 | faLifeRing, 12 | faSort, 13 | faArrowUp, 14 | faArrowDown, 15 | faChevronLeft, 16 | faChevronRight, 17 | faSortAlphaDown, 18 | faSortAlphaUp, 19 | faSignOutAlt 20 | ) 21 | 22 | Vue.component('font-awesome-icon', FontAwesomeIcon) 23 | 24 | dom.watch() // This will kick of the initial replacement of i to svg tags and configure a MutationObserver 25 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | import { formatNumber } from './numeral' 2 | 3 | export { 4 | formatNumber 5 | } 6 | -------------------------------------------------------------------------------- /src/filters/numeral.js: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral' 2 | 3 | export function formatNumber (value) { 4 | return numeral(value).format('0,0') 5 | } 6 | -------------------------------------------------------------------------------- /src/helpers/generate_csv.js: -------------------------------------------------------------------------------- 1 | function convertArrayToCSV (arrCSVContents) { 2 | if (arrCSVContents === undefined || arrCSVContents.length === 0) { 3 | return null 4 | } 5 | 6 | let keys = Object.keys(arrCSVContents[0]) 7 | let result = keys.join(',') + '\n' 8 | 9 | arrCSVContents.forEach((item) => { 10 | let arr = [] 11 | keys.forEach((key) => { 12 | arr.push(`"${item[key]}"`) 13 | }) 14 | result += arr.join(',') + '\n' 15 | }) 16 | 17 | return result 18 | } 19 | 20 | function generateCSV (filename, arrCSVContents) { 21 | let csv = convertArrayToCSV(arrCSVContents) 22 | if (csv === null) { 23 | return 24 | } 25 | 26 | var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) 27 | if (navigator.msSaveBlob) { // IE 10+ 28 | navigator.msSaveBlob(blob, filename) 29 | } else { 30 | var link = document.createElement('a') 31 | if (link.download !== undefined) { 32 | // Browsers that support HTML5 download attribute 33 | var url = URL.createObjectURL(blob) 34 | link.setAttribute('href', url) 35 | link.setAttribute('download', filename) 36 | link.style.visibility = 'hidden' 37 | document.body.appendChild(link) 38 | link.click() 39 | document.body.removeChild(link) 40 | } 41 | } 42 | } 43 | 44 | export default { 45 | generateCSV 46 | } 47 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | import { ADD_TOAST_MESSAGE } from '@/toast' 2 | 3 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 4 | 5 | export default { 6 | formatBytes (bytes, decimals) { 7 | if (bytes === 0) { 8 | return '0 Bytes' 9 | } 10 | let k = 1000 11 | let dm = decimals || 2 12 | let i = Math.floor(Math.log(bytes) / Math.log(k)) 13 | 14 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] 15 | }, 16 | 17 | timeDifference (current, previous) { 18 | var msPerMinute = 60 * 1000 19 | var msPerHour = msPerMinute * 60 20 | var msPerDay = msPerHour * 24 21 | var msPerMonth = msPerDay * 30 22 | var msPerYear = msPerDay * 365 23 | var elapsed = current - previous 24 | 25 | if (elapsed < msPerMinute) { 26 | return Math.round(elapsed / 1000) + ' seconds ago' 27 | } else if (elapsed < msPerHour) { 28 | return Math.round(elapsed / msPerMinute) + ' minutes ago' 29 | } else if (elapsed < msPerDay) { 30 | return Math.round(elapsed / msPerHour) + ' hours ago' 31 | } else if (elapsed < msPerMonth) { 32 | return 'approximately ' + Math.round(elapsed / msPerDay) + ' days ago' 33 | } else if (elapsed < msPerYear) { 34 | return 'approximately ' + Math.round(elapsed / msPerMonth) + ' months ago' 35 | } 36 | return 'approximately ' + Math.round(elapsed / msPerYear) + ' years ago' 37 | }, 38 | 39 | timeDifferenceFromNow (previous) { 40 | if (!(previous instanceof Date)) { 41 | previous = new Date(previous) 42 | } 43 | return this.timeDifference(new Date(), previous) 44 | }, 45 | 46 | // componentToastError is similiar to the helper in store/helpers but 47 | // works on a component level instead of store level 48 | componentToastError (component, error) { 49 | if (error.data.error) { 50 | component.$store.dispatch(ADD_TOAST_MESSAGE, { 51 | text: `${error.data.error}`, 52 | type: 'danger', 53 | dismissAfter: 4000 54 | }) 55 | } else { 56 | console.log('An error occurred: ', error) 57 | component.$store.dispatch(ADD_TOAST_MESSAGE, { 58 | text: `An unknown error occurred: ${error.message}`, 59 | type: 'danger', 60 | dismissAfter: 4000 61 | }) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/i18n/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GoCrack", 3 | "navbar": { 4 | "list_tasks": "Lists Tasks", 5 | "create_task": "Create Task", 6 | "file_mgr": "File Manager", 7 | "engine_files": "Engine Files", 8 | "task_files": "Task Files", 9 | "admin": "Admin Settings", 10 | "users": "Users", 11 | "edit_user": "Edit User", 12 | "version": "Version", 13 | "dark_mode": "Dark Mode" 14 | }, 15 | "login": { 16 | "header": "Log In", 17 | "placeholder_username": "Enter your username", 18 | "placeholder_password": "Enter your password", 19 | "button_login": "Login", 20 | "register": "Register", 21 | "validation": { 22 | "user_required": "Username is required", 23 | "user_min_length": "Username must be at least 4 characters", 24 | "email_required": "E-Mail is required", 25 | "email_valid": "E-Mail must be valid", 26 | "password_required": "Password is required", 27 | "password_min_length": "Password must be at least 6 characters", 28 | "empty_fields": "Username and/or Password cannot be empty" 29 | }, 30 | "username": "Username", 31 | "email": "E-Mail", 32 | "password": "Password", 33 | "created_user": "Created user! Logging you in...", 34 | "errors": { 35 | "unknown_error": "An unknown error occurred. This is probably due to a certificate error. Check Javascript Console for more information." 36 | } 37 | }, 38 | "errors": { 39 | "unauthorized": "Unauthorized", 40 | "unauthorized_text": "You do not have access to the requested resource", 41 | "header_server_error": "Server Error", 42 | "default_server_error": "An unknown server error occured. Please contact your site administrator", 43 | "upload_in_progress": "An upload is in progress. Please wait..." 44 | }, 45 | "create_task": { 46 | "placeholder_casecode": "CC-1337", 47 | "description_casecode": "The MPR or MFR number for the engagement.", 48 | "taskname": "Task Name", 49 | "placeholder_taskname": "Responder-Workstation-2017-03-27", 50 | "description_taskname": "Descriptive name for the task within the case. This can be simply a number (like 1, 2, etc), but it is better if you give it a more descriptive name for where the hashes came from (like Responder-Workstation-2017-03-27 indicating the network segment and date when the Responder tool was run).", 51 | "placeholder_comment": "Put any notes here that you'd like others to see...", 52 | "devices": "Use Devices", 53 | "placeholder_devices": "Select Devices (optional)", 54 | "description_devices": "Assign specific GPU devices or the CPU to this task. If specified and one or more devices are in use on other tasks, then this task will be queued until all devices are available. This field should typically be left blank and the system will automatically assign GPUs based on availability and task Priority.", 55 | "error_devices_mixed_types": "You cannot mix device types", 56 | "error_devices_mixed_hosts": "You cannot select devices from multiple hosts", 57 | "engine": "Engine", 58 | "description_engine": "The Cracking engine that should be used to process this task", 59 | "placeholder_engine": "Select Cracking Engine", 60 | "attack_mode": "Attack Mode", 61 | "description_attack_mode": "Dictionary/Straight attacks use a wordlist dictionary and an optional set of mangling rules. They are preferred since they are more likely to crack passwords in a reasonable amount of time. Brute Force attacks use a set of character type strings and try every possible password with those character types. Brute Force attacks are impractical for long passwords and should generally be performed only after an exhaustive Dictionary/Straight attack.", 62 | "attack_modes": { 63 | "brute_force": "Brute Force", 64 | "dict": "Dictionary/Straight" 65 | }, 66 | "dictionary": "Dictionary File", 67 | "description_dictionary": "Wordlist to use.", 68 | "mangling_file": "Mangling Rule(s)", 69 | "description_mangling_file": "Set of transformation rules that will be applied to each line in the dictionary. If no rules are selected, only the original line in the dictionary will be used.", 70 | "file": "Password(s) File", 71 | "description_file": "File containing hashes to be cracked. Exactly one Hash per line is required (no blank lines). Must be formatted exactly as shown for the selected Hash Type at the Example Hashes Guide. File will be checked on upload and task cannot be created if the file contains any errors.", 72 | "password_masks": "Password Mask(s)", 73 | "description_password_masks": "List of character type strings to use for Brute Force attack.", 74 | "hashtype": "Hash Type", 75 | "description_hashtype": "Type of hashes to be cracked. The number listed is the mode identifier for Hashcat. Must be selected before a file is uploaded.", 76 | "additional_users": "Additional Users", 77 | "description_users": "Users that will receive emails when passwords crack and who will have access to this task via this web interface.", 78 | "description_priority": "Relative priority of this task. Used to allocate GPUs and to prioritize tasks on the queue when necessary. If using a Higher priority, you may want to indicate why in the comment field.", 79 | "taskTimeLimit": "Runtime Limit", 80 | "taskTimeLimit_description": "The total runtime in seconds this job is allowed to run before it is automatically stopped. (default is 172,800 seconds, or 48 hours)", 81 | "error_taskTimeLimit": "The time limit must be expressed as an integer (in seconds)", 82 | "error_casecode": "The Case Code is required and must be at least 4 characters", 83 | "error_taskname": "The Task Name is required and must be at least 10 characters", 84 | "error_no_taskfile": "A file is required to crack", 85 | "placeholder_taskfile": "Select password(s) to crack", 86 | "invalid_form": "Please correct all validation errors before continuing", 87 | "error_invalid_engine": "Invalid Engine", 88 | "disable_optimization": "Disable Optimized Engine", 89 | "disable_optimization_description": "Checking this box will disable the optimized OpenCL kernel for hashcat. This allows you to crack passwords greater than length 32 at the cost of performance/cracking speed" 90 | }, 91 | "task_status": { 92 | "cracked_passwords_header": "Cracked Passwords", 93 | "task_status": "Task Status for ", 94 | "start": "Start", 95 | "stop": "Stop", 96 | "edit": "Edit", 97 | "download_results": "Download Results", 98 | "realtime_status": "Realtime Status", 99 | "task_info": "Task Info", 100 | "header_action": "Action", 101 | "header_status_code": "Status Code", 102 | "header_user_id": "User ID", 103 | "header_occurred_at": "Occurred At", 104 | "tooltip_realtime_offline": "Realtime Status Offline/Starting", 105 | "tooltip_edit_disabled": "Task must be stopped before editing", 106 | "header_audit_log": "Audit Log", 107 | "download_audit_log": "Export Audit Log", 108 | "using_devices": "Using Devices", 109 | "randomly_assigned": "Randomly Assigned", 110 | "file_information": "File Information", 111 | "status_changed": "Status changed successfully. It may take a moment for the change to take effect" 112 | }, 113 | "shared": { 114 | "upload_file": "Upload File", 115 | "download_file": "Download", 116 | "loading": "Loading...", 117 | "actions": "Actions", 118 | "edit": "Edit", 119 | "download": "Download", 120 | "files": "Files", 121 | "logout": "Logout", 122 | "close": "Close", 123 | "file_type": "File Type", 124 | "casecode": "Case Code", 125 | "name": "Name", 126 | "created_by": "Created By", 127 | "comment": "Comment", 128 | "status": "Status", 129 | "error": "Error", 130 | "created_at": "Created At", 131 | "timeout_at": "Timeout At", 132 | "language": "Language", 133 | "task_name": "Task Name", 134 | "delete": "Delete", 135 | "engine": "Engine", 136 | "search": "Search", 137 | "search_tasks": "Search Tasks", 138 | "edit_settings": "Edit Settings", 139 | "priority": "Priority", 140 | "choose_file": "Choose a File to Upload...", 141 | "license": "License", 142 | "submit": "Submit", 143 | "please_wait": "Please wait...", 144 | "disabled": "Disabled", 145 | "never": "Never" 146 | }, 147 | "upload_modal": { 148 | "engine": "Engine", 149 | "file": "File", 150 | "title": "Upload File", 151 | "select_hashcat_type": "Hash Type", 152 | "is_shared": "Is Shared?", 153 | "description_is_shared": "If selected, this allows other users who are not entitled to this file the ability to use it in their task. However, they will not be able to edit or download this file.", 154 | "uploading": "Uploading...", 155 | "all_selected": "Selecting 'All' performs no validation on the file you're about to upload and will show 0 hashes & 0 salts", 156 | "error_select_a_file": "You must select a file before continuing" 157 | }, 158 | "edit_user": { 159 | "email": "E-Mail Address", 160 | "is_admin": "Admin", 161 | "description_is_admin": "Is the user an administrator? This grants the user access to everything", 162 | "browser_notifications": "Browser Notifications", 163 | "notification_revoke": "Revoke", 164 | "notification_grant": "Grant", 165 | "current_password": "Current Password", 166 | "new_password": "New Password", 167 | "confirm_password": "Confirm Password", 168 | "email_invalid": "Please use a valid email address", 169 | "current_password_missing": "Please set your current password", 170 | "confirm_password_missing": "Please re-type your new password below", 171 | "header_change_password": "Change Password", 172 | "header_notifications": "Notifications", 173 | "errors": { 174 | "no_changes": "No changes detected" 175 | }, 176 | "revoke": "Revoke", 177 | "grant": "Grant" 178 | }, 179 | "shared_headers": { 180 | "file_id": "File ID", 181 | "hashes_n_salts": "Hashes / Salts", 182 | "filename": "File Name", 183 | "uploaded_by": "Uploaded By", 184 | "number_entries": "Item Count", 185 | "file_type": "File Type", 186 | "information": "Information", 187 | "running_tasks": "Running Tasks" 188 | }, 189 | "list_tasks": { 190 | "header_tasks": "Tasks", 191 | "header_task_id": "Task ID", 192 | "header_cracked_total": "Cracked / Unq." 193 | }, 194 | "list_files": { 195 | "header_small_task": "uncracked password files", 196 | "header_small_engine": "dictionaries, manging rules, brute force masks" 197 | }, 198 | "delete_modal": { 199 | "header_task": "Delete Task", 200 | "header_file": "Delete File", 201 | "warning_task": "Deleting this task will remove all cracked hashes along with the audit log. Are you sure you want to do this?

You may take the following actions before deleting:", 202 | "warning_file": "Deleting this file will cause tasks that rely on it to fail next time they attempt to run. Are you sure you want to do this?", 203 | "success_file": "Successfully deleted file", 204 | "success_task": "Successfully deleted task" 205 | }, 206 | "edit_modal": { 207 | "admin_header": "Admin Only", 208 | "modify_task_status": "Change Status", 209 | "modify_task_status_warn": "This allows you to override the tasks status. Only do this if you know what you're doing.", 210 | "modified_successfully": "Modified task {taskid}", 211 | "modified_failed": "Task Modification Failed for {taskid}" 212 | }, 213 | "stop_modal": { 214 | "confirmation": "Are you sure you want to stop this task?", 215 | "confirmation_description": "If the task has not checkpointed, the task will be started from the beginning if resumed.", 216 | "stop_ok": "The request to stop the job was successful. There may be a delay as the request is processed" 217 | }, 218 | "hashcat": { 219 | "settings_header": "Hashcat Settings", 220 | "attack_mode": "Attack Mode", 221 | "hash_type": "Hash Type", 222 | "mangling_rules": "Mangling Rule(s)", 223 | "bruteforce_masks": "Bruteforce Mask(s)", 224 | "no_status": "The task must be running in order for the live status to be displayed. If the task is running, please wait a few seconds for the browser to start recieving events.", 225 | "hash_target": "Hash Target", 226 | "guess_mask": "Guess Mask", 227 | "guess_base": "Guess Base", 228 | "guess_mod": "Guess Mod", 229 | "status": "Status", 230 | "time_started": "Time Started", 231 | "time_estimated": "Time Estimated", 232 | "total_speed": "Total Speed", 233 | "recovered": "Recovered", 234 | "guess_queue": "Guess Queue", 235 | "progress": "Progress", 236 | "restore_point": "Restore Point", 237 | "rejected": "Rejected" 238 | }, 239 | "dashboard": { 240 | "connected_workers": "Active Workers", 241 | "running_tasks": "Running Tasks", 242 | "devices": "Free / Total Devices" 243 | }, 244 | "graph": { 245 | "password_lengths": "Length of Password", 246 | "num_cracked_passwords": "Number of Cracked Passwords" 247 | }, 248 | "file_info": { 249 | "task_file": "Task File", 250 | "dictionary_file": "Dictionary File" 251 | }, 252 | "version": { 253 | "server": "Server Version", 254 | "ui": "UI Version", 255 | "compiled_at": "Compiled At", 256 | "worker_version_info": "Worker Version Info", 257 | "loading": "Loading version information...", 258 | "vue": "Vue.JS Version" 259 | }, 260 | "selector_file": { 261 | "file_id": "File ID: {id}", 262 | "uploaded_by": "Uploaded By: {by}", 263 | "num_entries": "# of Entries: {entries}" 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | /* Languages */ 5 | import en from './en-US.json' 6 | 7 | Vue.use(VueI18n) 8 | 9 | const languages = { 10 | default: 'en-US', 11 | options: { 12 | 'en-US': en 13 | } 14 | } 15 | 16 | export function createI18n ({ defaultLocale = languages.default }) { 17 | var i18n = new VueI18n({ 18 | locale: defaultLocale, 19 | messages: languages.options 20 | }) 21 | 22 | if (module.hot) { 23 | console.log('Allowing for hot reload of i18n translations') 24 | module.hot.accept(['./en-US.json'], () => { 25 | i18n.setLocaleMessage('en', require('./en-US.json')) 26 | console.log('Reloading translations') 27 | }) 28 | } 29 | 30 | return i18n 31 | } 32 | 33 | export const langs = languages 34 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import router from '@/router' 4 | import store from '@/store' 5 | import storeMutations from '@/store/mutations' 6 | import { createI18n } from './i18n' 7 | 8 | import { ServerTable, ClientTable } from 'vue-tables-2' 9 | import { sync } from 'vuex-router-sync' 10 | import axios from 'axios' 11 | import Vuelidate from 'vuelidate' 12 | import BootstrapVue from 'bootstrap-vue' 13 | import VuePlugin from '@/api' 14 | import * as Sentry from '@sentry/browser' 15 | import * as Integrations from '@sentry/integrations' 16 | 17 | import '@/faloader' 18 | import config from '@/config' 19 | 20 | import 'popper.js/dist/umd/popper.min.js' 21 | import 'bootstrap/dist/js/bootstrap.min.js' 22 | import 'vue-multiselect/dist/vue-multiselect.min.css' 23 | 24 | // Custom Components 25 | import * as CreateTaskComponents from './components/CreateTask' 26 | 27 | import * as HashcatComponents from './components/Hashcat' 28 | 29 | if (config.SENTRY_DSN !== undefined || (config.SENTRY_DSN !== undefined && config.SENTRY_DSN !== '')) { 30 | console.log('Sentry enabled') 31 | 32 | Sentry.init({ 33 | dsn: config.SENTRY_DSN, 34 | integrations: [new Integrations.Vue({ Vue, attachProps: true })] 35 | }) 36 | } 37 | 38 | const tableIcons = { 39 | base: 'fa fas', 40 | is: 'fa-sort', 41 | up: 'fa-sort-alpha-up', 42 | down: 'fa-sort-alpha-down' 43 | } 44 | 45 | const debug = process.env.NODE_ENV !== 'production' 46 | 47 | for (let component in CreateTaskComponents) { 48 | Vue.component(component, CreateTaskComponents[component]) 49 | } 50 | 51 | for (let component in HashcatComponents) { 52 | Vue.component(component, HashcatComponents[component]) 53 | } 54 | 55 | Vue.config.productionTip = false 56 | window.axios = axios 57 | 58 | Vue.use(Vuelidate) 59 | Vue.use(ClientTable, { sortIcon: tableIcons }, false, 'bootstrap4') 60 | Vue.use(ServerTable, { sortIcon: tableIcons }, false, 'bootstrap4') 61 | Vue.use(BootstrapVue) 62 | 63 | // Sync vue's router with the state backend 64 | sync(store, router) 65 | 66 | const i18n = createI18n({ defaultLocale: store.getters.getCurrentLanguage }) 67 | 68 | // install some interceptors into axios 69 | function installInterceptors () { 70 | axios.interceptors.response.use((response) => { 71 | return response 72 | }, error => { 73 | if (error.response) { 74 | var response = error.response 75 | // If we recieve a response that indicates our token has expired, force the user to relog 76 | if (response.status === 401 && (response.data.hasOwnProperty('expired') && response.data.expired)) { 77 | // console.log('intercepted an expired token. forcing state change') 78 | store.dispatch(storeMutations.LOGOUT) 79 | } 80 | } 81 | return Promise.reject(error.response) 82 | }) 83 | } 84 | 85 | installInterceptors() 86 | 87 | let BASE_URL = `${window.location.protocol}//${window.location.host}` 88 | 89 | if (debug) { 90 | BASE_URL = 'http://' + window.location.hostname + ':1338' 91 | } 92 | 93 | Vue.use(VuePlugin, { 94 | server: BASE_URL, 95 | base_endpoint: '/api/v2' 96 | }) 97 | 98 | store.dispatch('initializeAuthFromStorage').then(success => { 99 | // console.log('successfully re-initialized auth store') 100 | // asynchronously request permissions to browser notifications 101 | store.dispatch(storeMutations.NOTIFY_REQUESTED) 102 | /* eslint-disable no-new */ 103 | new Vue({ 104 | el: '#app', 105 | router, 106 | i18n, 107 | store, 108 | template: '', 109 | components: { App } 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /src/mixins/chCompValidator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | v: { 4 | type: Object, 5 | default: null 6 | } 7 | }, 8 | 9 | model: { 10 | prop: 'value' 11 | }, 12 | 13 | data () { 14 | return { internalValue: this.value } 15 | }, 16 | 17 | watch: { 18 | internalValue (val) { 19 | this.$emit('input', val) 20 | 21 | if (this.v !== null) { 22 | this.v.$touch() 23 | } 24 | } 25 | }, 26 | 27 | computed: { 28 | state () { 29 | // Do we have a validator? 30 | if (this.v === null) { 31 | return null 32 | } 33 | 34 | // Check our validity from the parent validator 35 | return this.v.$dirty ? !this.v.$invalid && !this.v.$error : null 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/mixins/index.js: -------------------------------------------------------------------------------- 1 | import chCompValidator from './chCompValidator' 2 | 3 | export { 4 | chCompValidator 5 | } 6 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import store from '@/store' 4 | 5 | import Dashboard from '@/views/Dashboard' 6 | import Login from '@/views/Login' 7 | import ListTasks from '@/views/ListTask' 8 | import CreateTask from '@/views/CreateTask' 9 | import TaskStatus from '@/views/TaskStatus' 10 | import ErrorUnauthorized from '@/views/ErrorUnauthorized' 11 | import ErrorServer from '@/views/ErrorServer' 12 | import ListFiles from '@/views/ListFiles' 13 | import UserListing from '@/views/UserListing' 14 | import ModifyUser from '@/views/ModifyUser' 15 | import RegisterUser from '@/views/RegisterUser' 16 | import VersionInfo from '@/views/VersionInfo' 17 | 18 | Vue.use(Router) 19 | 20 | const routes = [ 21 | { 22 | path: '/login', 23 | name: 'Login', 24 | component: Login, 25 | meta: { 26 | title: 'Login' 27 | } 28 | }, 29 | { 30 | path: '/', 31 | name: 'Dashboard', 32 | component: Dashboard, 33 | meta: { 34 | protectedWithLogin: true, 35 | title: 'Dashboard' 36 | } 37 | }, 38 | { 39 | path: '/tasks', 40 | name: 'Tasks', 41 | component: ListTasks, 42 | meta: { 43 | title: 'List Tasks', 44 | protectedWithLogin: true 45 | } 46 | }, 47 | { 48 | path: '/tasks/create', 49 | name: 'Create Task', 50 | component: CreateTask, 51 | meta: { 52 | title: 'Create Task', 53 | protectedWithLogin: true 54 | } 55 | }, 56 | { 57 | path: '/tasks/details/:id', 58 | name: 'Task Details', 59 | component: TaskStatus, 60 | meta: { 61 | title: 'Task Details', 62 | protectedWithLogin: true 63 | } 64 | }, 65 | { 66 | path: '/unauthorized', 67 | name: 'Unauthorized', 68 | component: ErrorUnauthorized, 69 | meta: { 70 | title: 'Unauthorized', 71 | protectedWithLogin: false 72 | } 73 | }, 74 | { 75 | path: '/servererror', 76 | name: 'System Error', 77 | component: ErrorServer, 78 | meta: { 79 | title: 'Error', 80 | protectedWithLogin: false 81 | } 82 | }, 83 | { 84 | path: '/files/engine', 85 | name: 'Engine Files', 86 | component: ListFiles, 87 | meta: { 88 | title: 'Engine Files', 89 | protectedWithLogin: true, 90 | key: 0 91 | }, 92 | props: { 93 | isTaskFile: false 94 | } 95 | }, 96 | { 97 | path: '/files/task', 98 | name: 'Task Files', 99 | component: ListFiles, 100 | meta: { 101 | title: 'Task Files', 102 | protectedWithLogin: true, 103 | key: 1 104 | }, 105 | props: { 106 | isTaskFile: true 107 | } 108 | }, 109 | { 110 | path: '/users', 111 | name: 'User Listing', 112 | component: UserListing, 113 | meta: { 114 | title: 'Users', 115 | protectedWithLogin: true, 116 | requireAdminToken: true 117 | } 118 | }, 119 | { 120 | path: '/users/edit', 121 | name: 'Edit Current User', 122 | component: ModifyUser, 123 | meta: { 124 | title: 'Modify User', 125 | protectedWithLogin: true, 126 | requireAdminToken: false 127 | } 128 | }, 129 | { 130 | path: '/users/:id', 131 | name: 'Edit User', 132 | component: ModifyUser, 133 | meta: { 134 | title: 'Modify User', 135 | protectedWithLogin: true, 136 | requireAdminToken: false 137 | } 138 | }, 139 | { 140 | path: '/register', 141 | name: 'Registration', 142 | component: RegisterUser, 143 | meta: { 144 | title: 'Register', 145 | preventIfLoggedIn: true 146 | } 147 | }, 148 | { 149 | path: '/version', 150 | name: 'Version Information', 151 | component: VersionInfo, 152 | meta: { 153 | title: 'Version Information' 154 | } 155 | } 156 | ] 157 | 158 | const router = new Router({ 159 | linkActiveClass: 'nav-item active', 160 | mode: 'history', 161 | routes: routes 162 | }) 163 | 164 | router.beforeEach((to, from, next) => { 165 | // Ensure the user is logged in if we're on a protected route 166 | if (to.matched.some(record => record.meta.protectedWithLogin)) { 167 | let user = store.state.auth 168 | if (!user.authenticated) { 169 | next({ 170 | path: '/login', 171 | query: { redirect: to.fullPath } 172 | }) 173 | return 174 | } 175 | } 176 | 177 | // Prevent the user to certain routes if meta contains preventIfLoggedIn and set to true 178 | if (to.matched.some(record => record.meta.preventIfLoggedIn)) { 179 | let user = store.state.auth 180 | if (user.authenticated) { 181 | next({ path: '/' }) 182 | return 183 | } 184 | } 185 | 186 | // If the route requires an admin token... 187 | if (to.matched.some(record => record.meta.requireAdminToken)) { 188 | let user = store.state.auth 189 | if (!user.is_admin) { 190 | next({ path: '/unauthorized' }) 191 | return 192 | } 193 | } 194 | 195 | if (to.meta.title) { 196 | document.title = `GoCrack - ${to.meta.title}` 197 | } else { 198 | document.title = `GoCrack` 199 | } 200 | 201 | next() 202 | }) 203 | 204 | export default router 205 | -------------------------------------------------------------------------------- /src/store/helpers.js: -------------------------------------------------------------------------------- 1 | import { ADD_TOAST_MESSAGE } from '@/toast' 2 | 3 | export default { 4 | handleAPIError (error, dispatch) { 5 | if (error.response) { 6 | dispatch(ADD_TOAST_MESSAGE, { 7 | text: `${error.response.data.error}`, 8 | type: 'danger', 9 | dismissAfter: 4000 10 | }) 11 | } else { 12 | dispatch(ADD_TOAST_MESSAGE, { 13 | text: `An unknown error occurred: ${error.message}`, 14 | type: 'danger', 15 | dismissAfter: 4000 16 | }) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import { createModule } from '@/toast' 5 | import authmod from '@/store/modules/authentication' 6 | import workersmod from '@/store/modules/workers' 7 | import realtimemod from '@/store/modules/realtime' 8 | import browsernotify from '@/store/modules/browser_notify' 9 | import usersettings from '@/store/modules/user_local_settings' 10 | 11 | Vue.use(Vuex) 12 | 13 | const debug = process.env.NODE_ENV !== 'production' 14 | 15 | const store = new Vuex.Store({ 16 | modules: { 17 | toast: createModule({ 18 | dismissInterval: 8000 19 | }), 20 | auth: authmod, 21 | realtime: realtimemod, 22 | workers: workersmod, 23 | browsernotify: browsernotify, 24 | usersettings: usersettings 25 | }, 26 | strict: debug 27 | }) 28 | 29 | if (module.hot) { 30 | module.hot.accept([ 31 | './modules/authentication', 32 | './modules/realtime', 33 | './modules/workers', 34 | './modules/browser_notify', 35 | './modules/user_local_settings' 36 | ], () => { 37 | store.hotUpdate({ 38 | modules: { 39 | auth: require('./modules/authentication'), 40 | realtime: require('./modules/realtime'), 41 | workers: require('./modules/workers'), 42 | browsernotify: require('./modules/browser_notify'), 43 | usersettings: require('./modules/user_local_settings') 44 | } 45 | }) 46 | }) 47 | } 48 | 49 | export default store 50 | -------------------------------------------------------------------------------- /src/store/modules/authentication.js: -------------------------------------------------------------------------------- 1 | import { ADD_TOAST_MESSAGE } from '@/toast' 2 | import muts from '@/store/mutations' 3 | import router from '@/router' 4 | 5 | import jwtdecode from 'jwt-decode' 6 | 7 | // initial state 8 | const state = { 9 | username: '', 10 | user_uuid: '', 11 | authenticated: false, 12 | is_admin: false, 13 | pending: false, 14 | token: '' 15 | } 16 | 17 | const getters = { 18 | isAuthPending: state => state.pending, 19 | userIsLoggedIn: state => state.authenticated, 20 | isAdministrator: state => state.is_admin, 21 | getAuthDetails: state => state, 22 | getCurrentAuthToken: state => state.token 23 | } 24 | 25 | const actions = { 26 | // performLogin takes the credentials from the view, passes it to the API, and 27 | // fires the correct mutator based on the validity of the login 28 | [muts.LOGIN_SUCCESS] ({ commit, rootState, dispatch }, response) { 29 | let { token } = response 30 | 31 | try { 32 | let decodedToken = jwtdecode(token) 33 | commit(muts.LOGIN_SUCCESS, { token, ...decodedToken }) 34 | dispatch(muts.REALTIME_CONNECTING) 35 | if (rootState.route.query.redirect) { 36 | router.push(rootState.route.query.redirect) 37 | } else { 38 | router.push('/') 39 | } 40 | } catch (err) { 41 | dispatch(ADD_TOAST_MESSAGE, { 42 | text: 'Login failed: Invalid JWT from Server', 43 | type: 'danger', 44 | dismissAfter: 4000 45 | }) 46 | 47 | console.log('An error occurred while validating the JWT: ', err) 48 | commit(muts.LOGIN_FAILED) 49 | } 50 | }, 51 | 52 | // destroy's the state for the authentication 53 | [muts.LOGOUT] ({ commit, dispatch }) { 54 | commit(muts.LOGOUT) 55 | dispatch(muts.REALTIME_LOGOUT) 56 | router.push('/login') 57 | 58 | dispatch(ADD_TOAST_MESSAGE, { 59 | text: 'Logout successful', 60 | type: 'info', 61 | dismissAfter: 4000 62 | }) 63 | }, 64 | 65 | // initializeAuthFromStorage should be called from the application's 66 | // entrypoint and (re)initializes the authentication state if it's valid 67 | initializeAuthFromStorage ({ commit, dispatch }) { 68 | var token = localStorage.getItem('auth_token') 69 | if (token === null || token === '') { 70 | commit(muts.LOGOUT) 71 | return 72 | } 73 | 74 | var nowSeconds = Math.round(new Date().getTime() / 1000) 75 | try { 76 | var resp = jwtdecode(token) 77 | if (resp.exp <= nowSeconds) { 78 | console.log('token has expired') 79 | commit(muts.LOGOUT) 80 | } else { 81 | console.log('Reloaded user state from local storage') 82 | commit(muts.LOGIN_SUCCESS, { ...resp, token }) 83 | dispatch(muts.REALTIME_CONNECTING) 84 | } 85 | } catch (err) { 86 | console.log(err) 87 | } 88 | } 89 | } 90 | 91 | const mutations = { 92 | [muts.LOGIN_PENDING] (state) { 93 | state.pending = true 94 | }, 95 | 96 | [muts.LOGIN_SUCCESS] (state, payload) { 97 | localStorage.setItem('auth_token', payload.token) 98 | state.username = payload.username 99 | state.user_uuid = payload.user_uuid 100 | state.is_admin = payload.is_admin 101 | state.pending = false 102 | state.authenticated = true 103 | state.token = payload.token 104 | }, 105 | 106 | [muts.LOGIN_FAILED] (state) { 107 | state.pending = false 108 | }, 109 | 110 | [muts.LOGOUT] (state) { 111 | state.is_admin = false 112 | state.authenticated = false 113 | state.username = '' 114 | state.user_uuid = '' 115 | localStorage.removeItem('auth_token') 116 | } 117 | } 118 | 119 | export default { 120 | state, 121 | getters, 122 | actions, 123 | mutations 124 | } 125 | -------------------------------------------------------------------------------- /src/store/modules/browser_notify.js: -------------------------------------------------------------------------------- 1 | import muts from '@/store/mutations' 2 | 3 | // initial state 4 | const state = () => { 5 | let supported = ('Notification' in window) 6 | if (!supported) { 7 | return { 8 | has_support: false, 9 | granted: false 10 | } 11 | } 12 | 13 | return { 14 | has_support: supported, 15 | granted: Notification.permission === 'granted', 16 | sentCrackedNotifications: {} 17 | } 18 | } 19 | 20 | const getters = { 21 | areNotificationsEnabled: state => state.granted, 22 | areNotificationsSupported: state => state.has_support 23 | } 24 | 25 | const actions = { 26 | [muts.NOTIFY_SEND] ({ commit, rootState, dispatch }, payload) { 27 | new Notification( // eslint-disable-line no-new 28 | payload.title || 'GoCrack Notification', 29 | { 30 | body: payload.body 31 | } 32 | ) 33 | }, 34 | 35 | [muts.NOTIFY_REQUESTED] ({ state, commit }) { 36 | if (state.granted) { 37 | return 38 | } 39 | 40 | Notification.requestPermission(function (permission) { 41 | if (permission === 'granted') { 42 | commit(muts.NOTIFY_GRANTED, { granted: true }) 43 | } else { 44 | commit(muts.NOTIFY_GRANTED, { granted: false }) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | const mutations = { 51 | [muts.NOTIFY_GRANTED] (state, { granted }) { 52 | state.granted = granted 53 | } 54 | } 55 | 56 | export default { 57 | state, 58 | getters, 59 | actions, 60 | mutations 61 | } 62 | -------------------------------------------------------------------------------- /src/store/modules/realtime.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import config from '@/api/config' 4 | import muts from '@/store/mutations' 5 | 6 | const state = () => { 7 | return { 8 | es: null, 9 | connecting: false, 10 | connected: false, 11 | taskEngineStatus: {}, // stores realtime information about a task engine 12 | taskStatuses: {} // stores the task statuses 13 | } 14 | } 15 | 16 | const getters = { 17 | // getTaskEngineStatusById returns the current engine status 18 | // if we're recieving realtime messages for this task. 19 | getTaskEngineStatusById: (state) => (id) => { 20 | if (state.taskEngineStatus.hasOwnProperty(id)) { 21 | return state.taskEngineStatus[id] 22 | } 23 | return { running: false, status: {} } 24 | }, 25 | 26 | // getTaskStatusByID returns the current task status ("running", "stopped", etc) 27 | // if we're recieving realtime messages for this task. Otherwise we return `defaultStatus` 28 | getTaskStatusByID: (state) => (id, defaultStatus = 'Unknown') => { 29 | if (state.taskStatuses.hasOwnProperty(id)) { 30 | return state.taskStatuses[id] 31 | } 32 | return defaultStatus 33 | } 34 | } 35 | 36 | const actions = { 37 | [muts.REALTIME_CONNECTING] ({ commit, state, rootState, dispatch }) { 38 | commit(muts.REALTIME_CONNECTING) 39 | 40 | let { server, base_endpoint } = config.get 41 | var es = new EventSource(`${server}${base_endpoint}/realtime/`, { withCredentials: true }) /* eslint camelcase: "off" */ 42 | 43 | es.addEventListener('open', function () { 44 | commit(muts.REALTIME_CONNECTED, { es }) 45 | }) 46 | 47 | es.addEventListener('message', function (event) { 48 | dispatch(muts.REALTIME_RECV_MSG, JSON.parse(event.data)) 49 | }) 50 | 51 | es.addEventListener('error', () => { 52 | commit(muts.REALTIME_DISCONNECTED) 53 | }) 54 | }, 55 | 56 | [muts.REALTIME_RECV_MSG] ({ commit, rootState, dispatch }, payload) { 57 | var { message, topic } = payload 58 | if (process.env.NODE_ENV !== 'production') { 59 | console.log(`REALTIME_RECV_MSG for topic ${topic}`, message) 60 | } 61 | 62 | switch (topic) { 63 | case 'cracked_password': 64 | // XXX(cschmitt): Browser notifications 65 | break 66 | case 'task_status': 67 | switch (message.status.toLowerCase()) { 68 | case 'queued': 69 | case 'dequeued': 70 | break 71 | default: 72 | dispatch(muts.NOTIFY_SEND, { title: 'Task Status Change', body: `${message.status} for ${message.task_id}` }) 73 | break 74 | } 75 | commit(muts.REALTIME_TASK_STATUS, { message }) 76 | break 77 | case 'task_engine_status': 78 | commit(muts.REALTIME_TASK_ENGINE_STATUS, { statusMessage: message, final: false }) 79 | break 80 | case 'task_status_final': 81 | commit(muts.REALTIME_TASK_ENGINE_STATUS, { statusMessage: message, final: true }) 82 | break 83 | } 84 | }, 85 | 86 | [muts.REALTIME_LOGOUT] ({ commit }) { 87 | commit(muts.REALTIME_LOGOUT) 88 | } 89 | } 90 | 91 | const mutations = { 92 | [muts.REALTIME_TASK_STATUS] (state, { message }) { 93 | Vue.set(state.taskStatuses, message.task_id, message.status) 94 | }, 95 | 96 | [muts.REALTIME_TASK_ENGINE_STATUS] (state, { statusMessage, final }) { 97 | // Have to use Vue.set to trigger the update in the reactivity system 98 | Vue.set(state.taskEngineStatus, statusMessage.task_id, { 99 | status: statusMessage.status, 100 | updated: new Date(), 101 | running: !final, 102 | stopped: final 103 | }) 104 | }, 105 | 106 | [muts.REALTIME_LOGOUT] (state) { 107 | if (state.es !== null) { 108 | state.es.close() 109 | } 110 | state.connected = false 111 | state.es = null 112 | }, 113 | 114 | [muts.REALTIME_DISCONNECTED] (state) { 115 | state.connected = false 116 | state.es = null 117 | }, 118 | 119 | [muts.REALTIME_CONNECTING] (state) { 120 | state.connecting = true 121 | state.connected = false 122 | }, 123 | 124 | [muts.REALTIME_CONNECTED] (state, { es }) { 125 | state.es = es 126 | state.connecting = false 127 | state.connected = true 128 | } 129 | } 130 | 131 | export default { 132 | state, 133 | getters, 134 | actions, 135 | mutations 136 | } 137 | -------------------------------------------------------------------------------- /src/store/modules/user_local_settings.js: -------------------------------------------------------------------------------- 1 | import { langs } from '@/i18n' 2 | import muts from '@/store/mutations' 3 | 4 | const state = () => { 5 | return { 6 | language: localStorage.getItem('i18_lang') || langs.default 7 | } 8 | } 9 | 10 | const getters = { 11 | getAvailableLanguages () { 12 | return Object.keys(langs.options) 13 | }, 14 | getCurrentLanguage: state => state.language 15 | } 16 | 17 | const actions = { 18 | [muts.SET_LANGUAGE] ({ commit, rootState, dispatch }, props) { 19 | commit(muts.SET_LANGUAGE, { language: props.language }) 20 | } 21 | } 22 | 23 | const mutations = { 24 | [muts.SET_LANGUAGE] (state, { language }) { 25 | localStorage.setItem('i18_lang', language) 26 | state.language = language 27 | } 28 | } 29 | 30 | export default { 31 | state, 32 | getters, 33 | actions, 34 | mutations 35 | } 36 | -------------------------------------------------------------------------------- /src/store/modules/workers.js: -------------------------------------------------------------------------------- 1 | import muts from '@/store/mutations' 2 | 3 | // initial state 4 | const state = { 5 | hosts: [], 6 | loading: false, 7 | refreshing: false 8 | } 9 | 10 | const getters = { 11 | getHosts: state => state.hosts, 12 | isWorkersLoading: state => state.loading 13 | } 14 | 15 | const mutations = { 16 | [muts.WORKER_STATUS_GET] (state) { 17 | state.loading = true 18 | }, 19 | 20 | [muts.WORKER_STATUS_REFRESHING] (state) { 21 | state.loading = false 22 | state.refreshing = true 23 | }, 24 | 25 | [muts.WORKER_STATUS_RETRIEVED] (state, workers) { 26 | for (let i in workers) { 27 | let connectedHost = workers[i] 28 | connectedHost.devices = connectedHost.devices.map((dev) => { 29 | dev.hostname = connectedHost.hostname 30 | dev.displayname = `${dev.name} (ID: ${dev.id})` 31 | return dev 32 | }) 33 | } 34 | 35 | state.loading = false 36 | state.refreshing = false 37 | state.hosts = workers 38 | }, 39 | 40 | [muts.WORKER_STATUS_FAILED] (state) { 41 | state.loading = false 42 | state.refreshing = false 43 | } 44 | } 45 | 46 | export default { 47 | state, 48 | getters, 49 | mutations 50 | } 51 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Auth 3 | LOGIN_PENDING: 'LOGIN_PENDING', 4 | LOGIN_SUCCESS: 'LOGIN_SUCCESS', 5 | LOGIN_FAILED: 'LOGIN_FAILED', 6 | LOGOUT: 'LOGOUT', 7 | 8 | // Realtime 9 | REALTIME_CONNECTING: 'REALTIME_CONNECTING', 10 | REALTIME_CONNECTED: 'REALTIME_CONNECTED', 11 | REALTIME_DISCONNECTED: 'REALTIME_DISCONNECTED', 12 | REALTIME_LOGOUT: 'REALTIME_LOGOUT', 13 | REALTIME_RECV_MSG: 'REALTIME_RECV_MSG', 14 | REALTIME_TASK_STATUS: 'REALTIME_TASK_STATUS', 15 | REALTIME_TASK_ENGINE_STATUS: 'REALTIME_TASK_ENGINE_STATUS', 16 | REALTIME_CRACKED_PASSWORDS: 'REALTIME_CRACKED_PASSWORD', 17 | 18 | // Worker Status 19 | WORKER_STATUS_GET: 'WORKER_STATUS_GET', 20 | WORKER_STATUS_REFRESHING: 'WORKER_STATUS_REFRESHING', 21 | WORKER_STATUS_RETRIEVED: 'WORKER_STATUS_RETRIEVED', 22 | WORKER_STATUS_FAILED: 'WORKER_STATUS_FAILED', 23 | 24 | // Task Related 25 | TASKS: 'TASKS', 26 | TASKS_RETRIEVED: 'TASKS_RETRIEVED', 27 | TASKS_FAILED: 'TASKS_FAILED', 28 | 29 | HASH_TYPES: 'HASH_TYPES', 30 | HASH_TYPES_RETRIEVED: 'HASH_TYPES_RETRIEVED', 31 | HASH_TYPES_FAILED: 'HASH_TYPES_FAILED', 32 | 33 | NOTIFY_REQUESTED: 'NOTIFY_REQUESTED', 34 | NOTIFY_GRANTED: 'NOTIFY_GRANTED', 35 | NOTIFY_SEND: 'NOTIFY_SEND', 36 | 37 | SET_LANGUAGE: 'SET_LANGUAGE' 38 | } 39 | -------------------------------------------------------------------------------- /src/toast/README.md: -------------------------------------------------------------------------------- 1 | # toast 2 | 3 | Based on [vuex-toast](https://github.com/ktsn/vuex-toast/), this supports Bootstrap 4 -------------------------------------------------------------------------------- /src/toast/Toast.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | -------------------------------------------------------------------------------- /src/toast/config.js: -------------------------------------------------------------------------------- 1 | export const DefaultTransition = { 2 | functional: true, 3 | render (h, { children }) { 4 | const data = { 5 | attrs: { tag: 'div', name: 'toast', type: 'transition' } 6 | } 7 | return h('transition-group', data, children) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/toast/index.js: -------------------------------------------------------------------------------- 1 | import Toast from './Toast.vue' 2 | import { update } from './utils' 3 | 4 | export function createComponent (options = {}) { 5 | const { 6 | transition 7 | } = options 8 | 9 | return update(Toast, { 10 | components: { 11 | toastTransition: transition 12 | } 13 | }) 14 | } 15 | 16 | export { Toast } 17 | export * from './module' 18 | -------------------------------------------------------------------------------- /src/toast/module.js: -------------------------------------------------------------------------------- 1 | const PREFIX = '@@toast/' 2 | 3 | const ADD = `${PREFIX}ADD_TOAST_MESSAGE` 4 | const REMOVE = `${PREFIX}REMOVE_TOAST_MESSAGE` 5 | 6 | export { 7 | ADD as ADD_TOAST_MESSAGE, 8 | REMOVE as REMOVE_TOAST_MESSAGE 9 | } 10 | 11 | function createMessage (id, text, type, dismissAfter) { 12 | return { 13 | id, 14 | text, 15 | type, 16 | dismissAfter 17 | } 18 | } 19 | 20 | export function createModule (options = {}) { 21 | const { 22 | dismissInterval = 5000 23 | } = options 24 | 25 | let maxToastId = 0 26 | 27 | const state = { 28 | messages: [] 29 | } 30 | 31 | const getters = { 32 | toastMessages: (state) => state.messages 33 | } 34 | 35 | const actions = { 36 | [ADD] ({ commit }, { text, type = 'info', dismissAfter = dismissInterval }) { 37 | const id = ++maxToastId 38 | commit(ADD, createMessage(id, text, type, dismissAfter)) 39 | setTimeout(() => commit(REMOVE, id), dismissAfter) 40 | }, 41 | 42 | [REMOVE] ({ commit }, id) { 43 | commit(REMOVE, id) 44 | } 45 | } 46 | 47 | const mutations = { 48 | [ADD] (state, data) { 49 | state.messages.push(data) 50 | }, 51 | 52 | [REMOVE] (state, id) { 53 | state.messages = state.messages.filter(m => m.id !== id) 54 | } 55 | } 56 | 57 | return { 58 | state, 59 | getters, 60 | actions, 61 | mutations 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/toast/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Simple update without mutation 5 | */ 6 | export function update (obj, updater) { 7 | const res = {} 8 | Object.keys(obj).forEach(key => { 9 | res[key] = updater[key] === undefined ? obj[key] : updater[key] 10 | }) 11 | return res 12 | } 13 | -------------------------------------------------------------------------------- /src/validators/device_selection.js: -------------------------------------------------------------------------------- 1 | export function isDeviceTypeSelectionValid (selectedOptions) { 2 | // if length is 1 or less, it's obviously not going to contain mixed devices 3 | if (selectedOptions.length <= 1) { 4 | return true 5 | } 6 | 7 | let firstDevice = selectedOptions[0] 8 | let isValid = true 9 | selectedOptions.forEach((elem) => { 10 | if (elem.type !== firstDevice.type) { 11 | isValid = false 12 | } 13 | }) 14 | 15 | return isValid 16 | } 17 | 18 | export function areDevicesOnSameHost (selectedOptions) { 19 | if (selectedOptions.length <= 1) { 20 | return true 21 | } 22 | 23 | let firstDevice = selectedOptions[0] 24 | let isValid = true 25 | selectedOptions.forEach((elem) => { 26 | if (elem.hostname !== firstDevice.hostname) { 27 | isValid = false 28 | } 29 | }) 30 | 31 | return isValid 32 | } 33 | -------------------------------------------------------------------------------- /src/validators/index.js: -------------------------------------------------------------------------------- 1 | import { isDeviceTypeSelectionValid, areDevicesOnSameHost } from './device_selection' 2 | 3 | export { 4 | isDeviceTypeSelectionValid, 5 | areDevicesOnSameHost 6 | } 7 | -------------------------------------------------------------------------------- /src/views/CreateTask.vue: -------------------------------------------------------------------------------- 1 | 174 | 175 | 180 | 181 | 486 | -------------------------------------------------------------------------------- /src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 45 | 46 | 144 | -------------------------------------------------------------------------------- /src/views/ErrorServer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /src/views/ErrorUnauthorized.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /src/views/ListFiles.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 206 | -------------------------------------------------------------------------------- /src/views/ListTask.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 98 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 100 | -------------------------------------------------------------------------------- /src/views/ModifyUser.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 305 | -------------------------------------------------------------------------------- /src/views/RegisterUser.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 77 | 78 | 161 | -------------------------------------------------------------------------------- /src/views/TaskStatus.vue: -------------------------------------------------------------------------------- 1 | 184 | 185 | 198 | 199 | 418 | -------------------------------------------------------------------------------- /src/views/UserListing.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 45 | -------------------------------------------------------------------------------- /src/views/VersionInfo.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 87 | -------------------------------------------------------------------------------- /static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/gocrack-ui/a6f0b8733ab7b2a60c24052dc690d568abbc74b5/static/img/logo.png -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | lintOnSave: true, 5 | runtimeCompiler: true, 6 | productionSourceMap: true, 7 | configureWebpack: config => { 8 | var plugins = [ 9 | new webpack.ProvidePlugin({ 10 | $: 'jquery', 11 | jQuery: 'jquery', 12 | 'window.jQuery': 'jquery', 13 | 'Popper': 'popper.js' 14 | }) 15 | ] 16 | 17 | return { plugins: plugins } 18 | } 19 | } 20 | --------------------------------------------------------------------------------