├── .nvmrc ├── test ├── data │ └── .gitkeep └── aws-credentials.js ├── .yarn ├── versions │ ├── 05685397.yml │ ├── 157cfac6.yml │ ├── 469ecdcb.yml │ └── abf52e68.yml └── plugins │ └── @yarnpkg │ └── plugin-outdated.cjs ├── images ├── icon.icns ├── icon.ico ├── icon.png └── icon@2x.icns ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── main │ ├── api │ │ ├── response.js │ │ ├── config.json │ │ ├── routes │ │ │ └── auth.js │ │ ├── reloader │ │ │ ├── manager.js │ │ │ └── reloader.js │ │ ├── server.js │ │ ├── auth.js │ │ ├── server-config.js │ │ ├── storage.js │ │ └── aws-credentials.js │ ├── containers │ │ ├── logout.js │ │ ├── select-role.js │ │ ├── index.js │ │ ├── auth.js │ │ ├── refresh-jit.js │ │ ├── refresh.js │ │ └── configure.js │ ├── preload.js │ ├── touchbar.js │ ├── protocol.js │ ├── menu.js │ └── index.js └── renderer │ ├── containers │ ├── components │ │ ├── Logo.js │ │ ├── Error.js │ │ ├── DebugRoute.js │ │ └── InputGroupWithCopyButton.js │ ├── ErrorBoundary.js │ ├── refresh │ │ ├── Logout.js │ │ ├── Credentials.js │ │ └── Refresh.js │ ├── select-role │ │ ├── Role.js │ │ └── SelectRole.js │ ├── configure │ │ ├── LoginList.js │ │ ├── Configure.js │ │ ├── ConfigureMetadata.js │ │ ├── RecentLogins.js │ │ └── Login.js │ └── App.js │ ├── constants │ ├── hooks.js │ └── styles.js │ └── index.js ├── babel.config.js ├── .gitattributes ├── .github ├── renovate.json └── workflows │ └── node.js.yml ├── jest.config.js ├── craco.config.js ├── .yarnrc.yml ├── cortex.yaml ├── brew └── cask │ └── awsaml.rb ├── .gitignore ├── .editorconfig ├── LICENSE.md ├── .eslintrc.js ├── CODE_OF_CONDUCT.md ├── forge.config.js ├── package.json ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | node 2 | -------------------------------------------------------------------------------- /test/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarn/versions/05685397.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarn/versions/157cfac6.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarn/versions/469ecdcb.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarn/versions/abf52e68.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/awsaml/HEAD/images/icon.icns -------------------------------------------------------------------------------- /images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/awsaml/HEAD/images/icon.ico -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/awsaml/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/icon@2x.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/awsaml/HEAD/images/icon@2x.icns -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/awsaml/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/main/api/response.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | platform: process.platform, 3 | title: 'Rapid7 - Awsaml', 4 | }; 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { 3 | targets: { 4 | node: 'current', 5 | }, 6 | }, 7 | ]], 8 | }; 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>rapid7/renovate-config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | collectCoverage: true, 4 | coverageDirectory: 'coverage', 5 | testMatch: [ 6 | '**/test/**/*.js', 7 | ], 8 | reporters: [ 9 | 'default', 10 | ['jest-junit', { outputName: 'test-results.xml' }], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /src/main/api/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "host": "localhost", 4 | "port": 2600 5 | }, 6 | "auth": { 7 | "callbackUrl": "http://localhost:2600/sso/saml", 8 | "path": "/sso/saml", 9 | "audience": "http://localhost:2600/sso/saml" 10 | }, 11 | "aws": { 12 | "duration": 3600 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | module.exports = { 3 | webpack: { 4 | configure: (webpackConfig, { paths }) => { 5 | webpackConfig.entry = `${__dirname}/src/renderer/index.js`; 6 | paths.appIndexJs = `${__dirname}/src/renderer/index.js`; 7 | 8 | return webpackConfig; 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Awsaml", 3 | "name": "Awsaml", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/main/api/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | authHandler, 4 | } = require('../../containers/auth'); 5 | 6 | const router = express.Router(); 7 | 8 | module.exports = (app, auth) => { 9 | router.post('/', auth.authenticate('saml', { 10 | failureFlash: true, 11 | failureRedirect: app.get('configureUrl'), 12 | }), authHandler(app)); 13 | 14 | return router; 15 | }; 16 | -------------------------------------------------------------------------------- /src/renderer/containers/components/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const LogoImg = styled.img` 5 | margin-bottom: 15px; 6 | `; 7 | 8 | function Logo() { 9 | return ( 10 | 14 | ); 15 | } 16 | 17 | export default Logo; 18 | -------------------------------------------------------------------------------- /src/main/api/reloader/manager.js: -------------------------------------------------------------------------------- 1 | class ReloadManager { 2 | reloaders = {}; 3 | 4 | get(name) { 5 | return this.reloaders[name]; 6 | } 7 | 8 | add(reloader) { 9 | this.reloaders[reloader.name] = reloader; 10 | } 11 | 12 | removeByName(name) { 13 | delete this.reloaders[name]; 14 | } 15 | 16 | removeByReloader(reloader) { 17 | delete this.reloaders[reloader.name]; 18 | } 19 | } 20 | 21 | module.exports = () => new ReloadManager(); 22 | -------------------------------------------------------------------------------- /src/main/api/server.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.json'); 2 | const Auth = require('./auth'); 3 | 4 | const sessionSecret = process.env.SESSION_SECRET; 5 | 6 | const auth = new Auth(config.auth); 7 | const app = require('./server-config')(auth, config, sessionSecret); 8 | const authRoute = require('./routes/auth')(app, auth); 9 | 10 | app.use(config.auth.path, authRoute); 11 | app.all('*', auth.guard); 12 | 13 | module.exports = { 14 | app, 15 | auth, 16 | }; 17 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | packageExtensions: 8 | eslint-plugin-flowtype@*: 9 | peerDependenciesMeta: 10 | "@babel/plugin-syntax-flow": 11 | optional: true 12 | "@babel/plugin-transform-react-jsx": 13 | optional: true 14 | 15 | plugins: 16 | - path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs 17 | spec: "https://mskelton.dev/yarn-outdated/v3" 18 | 19 | yarnPath: .yarn/releases/yarn-4.9.2.cjs 20 | -------------------------------------------------------------------------------- /cortex.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | info: 3 | title: Awsaml 4 | description: 'Awsaml is an application for providing automatically rotated temporary 5 | AWS credentials. ' 6 | x-cortex-git: 7 | github: 8 | alias: r7org 9 | repository: rapid7/awsaml 10 | x-cortex-tag: awsaml 11 | x-cortex-type: service 12 | x-cortex-groups: 13 | - critical 14 | - publicly-accessible 15 | - exposure:opensource 16 | x-cortex-domain-parents: 17 | - tag: pd-services 18 | openapi: 3.0.1 19 | servers: 20 | - url: "/" 21 | -------------------------------------------------------------------------------- /brew/cask/awsaml.rb: -------------------------------------------------------------------------------- 1 | cask "awsaml" do 2 | version "4.0.0" 3 | sha256 "1cf9093156370380b328e3250a3ff7649968aee78c8bfcb09badbcdec05e36a0" 4 | 5 | url "https://github.com/rapid7/awsaml/releases/download/v#{version}/Awsaml-darwin-universal-#{version}.zip" 6 | name "awsaml" 7 | desc "Awsaml is an application for providing automatically rotated temporary AWS credentials." 8 | homepage "https://github.com/rapid7/awsaml" 9 | 10 | livecheck do 11 | url :url 12 | strategy :github_latest 13 | end 14 | 15 | app "Awsaml.app" 16 | end 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | /test/.aws/ 9 | /test/data/test.json 10 | 11 | # production 12 | /build 13 | /out 14 | /dist 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | *~ 27 | .pnp.* 28 | .yarn/* 29 | !.yarn/patches 30 | !.yarn/plugins 31 | !.yarn/releases 32 | !.yarn/sdks 33 | !.yarn/versions 34 | test-results.xml 35 | -------------------------------------------------------------------------------- /src/main/containers/logout.js: -------------------------------------------------------------------------------- 1 | const { 2 | app, 3 | } = require('../api/server'); 4 | 5 | async function logout() { 6 | const session = Storage.get('session'); 7 | const profileName = `awsaml-${session.accountId}`; 8 | const reloader = Manager.get(profileName); 9 | reloader.stop(); 10 | Manager.removeByReloader(reloader); 11 | 12 | Storage.set('session', {}); 13 | app.set('entryPointUrl', null); 14 | Storage.set('authenticated', false); 15 | Storage.set('multipleRoles', false); 16 | 17 | return { 18 | logout: true, 19 | }; 20 | } 21 | 22 | module.exports = { 23 | logout, 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [18.x, 20.x] 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: "yarn" 21 | - name: Install dependencies 22 | run: yarn install --immutable 23 | - name: Lint 24 | run: yarn lint 25 | - name: Unit Tests 26 | run: yarn test 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | tab_width = 2 13 | continuation_indent_size = 4 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | max_line_length = 120 18 | 19 | 20 | # Matches multiple files with brace expansion notation 21 | # Set default charset 22 | [*.js] 23 | quote_type = single 24 | curly_bracket_next_line = false 25 | spaces_around_operators = true 26 | spaces_around_brackets = none 27 | indent_brace_style = BSD KNF 28 | 29 | 30 | [*.html] 31 | quote_type = double 32 | -------------------------------------------------------------------------------- /src/renderer/constants/hooks.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | // stolen shamelessly from Dan Abramov: https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 4 | function useInterval(callback, delay) { 5 | const savedCallback = useRef(); 6 | 7 | // Remember the latest callback. 8 | useEffect(() => { 9 | savedCallback.current = callback; 10 | }, [callback]); 11 | 12 | // Set up the interval. 13 | // eslint-disable-next-line consistent-return 14 | useEffect(() => { 15 | function tick() { 16 | savedCallback.current(); 17 | } 18 | if (delay !== null) { 19 | const id = setInterval(tick, delay); 20 | return () => clearInterval(id); 21 | } 22 | }, [delay]); 23 | } 24 | 25 | export default useInterval; 26 | -------------------------------------------------------------------------------- /src/renderer/containers/components/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Alert } from 'reactstrap'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | 6 | function Error(props) { 7 | const { 8 | error, 9 | metadataUrlValid, 10 | } = props; 11 | 12 | return (error || metadataUrlValid === false) ? ( 13 | 14 | 15 | {` ${error}`} 16 | 17 | ) : ''; 18 | } 19 | 20 | Error.propTypes = { 21 | error: PropTypes.string, 22 | metadataUrlValid: PropTypes.bool, 23 | }; 24 | 25 | Error.defaultProps = { 26 | error: '', 27 | metadataUrlValid: true, 28 | }; 29 | 30 | export default Error; 31 | -------------------------------------------------------------------------------- /src/main/api/reloader/reloader.js: -------------------------------------------------------------------------------- 1 | class Reloader { 2 | intervalId = null; 3 | 4 | constructor({ 5 | name, callback, interval, role = null, 6 | }) { 7 | this.name = name; 8 | this.callback = callback; 9 | this.interval = interval; 10 | this.role = role; 11 | } 12 | 13 | setCallback(callback) { 14 | this.callback = callback; 15 | } 16 | 17 | start() { 18 | this.intervalId = setInterval(this.callback, this.interval); 19 | } 20 | 21 | stop() { 22 | clearInterval(this.intervalId); 23 | } 24 | 25 | restart() { 26 | this.stop(); 27 | this.start(); 28 | } 29 | 30 | setResponse(response) { 31 | this.response = response; 32 | } 33 | 34 | getResponse() { 35 | return this.response; 36 | } 37 | } 38 | 39 | module.exports = Reloader; 40 | -------------------------------------------------------------------------------- /src/renderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import 'bootstrap/dist/css/bootstrap.min.css'; 4 | import { library } from '@fortawesome/fontawesome-svg-core'; 5 | import { 6 | faCopy, 7 | faTrashAlt, 8 | } from '@fortawesome/free-regular-svg-icons'; 9 | import { 10 | faSearch, 11 | faCaretRight, 12 | faCaretDown, 13 | faExclamationTriangle, 14 | } from '@fortawesome/free-solid-svg-icons'; 15 | import { 16 | faAws, 17 | } from '@fortawesome/free-brands-svg-icons'; 18 | import App from './containers/App'; 19 | 20 | library.add(faCopy, faTrashAlt, faSearch, faCaretRight, faCaretDown, faExclamationTriangle, faAws); 21 | 22 | const container = document.getElementById('root'); 23 | const root = createRoot(container); 24 | root.render(); 25 | -------------------------------------------------------------------------------- /src/renderer/containers/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class ErrorBoundary extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | hasError: false, 10 | }; 11 | } 12 | 13 | componentDidCatch() { 14 | // Display fallback UI 15 | this.setState({ 16 | hasError: true, 17 | }); 18 | } 19 | 20 | render() { 21 | const { 22 | hasError, 23 | } = this.state; 24 | const { 25 | children, 26 | } = this.props; 27 | 28 | if (hasError) { 29 | return ( 30 |
31 |

Something went wrong.

32 |
33 | ); 34 | } 35 | 36 | return children; 37 | } 38 | } 39 | 40 | ErrorBoundary.propTypes = { 41 | children: PropTypes.element.isRequired, 42 | }; 43 | 44 | export default ErrorBoundary; 45 | -------------------------------------------------------------------------------- /src/renderer/containers/components/DebugRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Container, 4 | Row, 5 | Col, 6 | } from 'reactstrap'; 7 | import { useLocation } from 'react-router-dom'; 8 | 9 | const COLUMN_STYLE = { 10 | fontSize: '1.2rem', 11 | }; 12 | 13 | const generateDebugReport = (hash, pathname, search) => ` 14 | pathname: ${pathname} 15 | search: ${search} 16 | hash: ${hash} 17 | `.trim(); 18 | 19 | function DebugRoute() { 20 | const location = useLocation(); 21 | 22 | return ( 23 | 24 | 25 | 26 | Route: 27 |
28 |             {generateDebugReport({
29 |               hash: location.hash,
30 |               pathname: location.pathname,
31 |               search: location.search,
32 |             })}
33 |           
34 | 35 |
36 |
37 | ); 38 | } 39 | 40 | export default DebugRoute; 41 | -------------------------------------------------------------------------------- /src/renderer/containers/refresh/Logout.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Button } from 'reactstrap'; 4 | import { Navigate } from 'react-router-dom'; 5 | import styled from 'styled-components'; 6 | import { BUTTON_MARGIN } from '../../constants/styles'; 7 | 8 | const ButtonWithMargin = styled(Button)`${BUTTON_MARGIN}`; 9 | 10 | function Logout({ darkMode }) { 11 | const [logout, setLogout] = useState(false); 12 | 13 | const handleLogoutEvent = async (event) => { 14 | event.preventDefault(); 15 | 16 | const data = await window.electronAPI.logout(); 17 | setLogout(data.logout); 18 | }; 19 | 20 | if (logout) { 21 | return ; 22 | } 23 | 24 | return ( 25 | 30 | Logout 31 | 32 | ); 33 | } 34 | 35 | Logout.propTypes = { 36 | darkMode: PropTypes.bool.isRequired, 37 | }; 38 | 39 | export default Logout; 40 | -------------------------------------------------------------------------------- /src/main/containers/select-role.js: -------------------------------------------------------------------------------- 1 | async function getRoles() { 2 | const session = Storage.get('session'); 3 | 4 | if (!session) { 5 | return { 6 | error: 'Invalid session', 7 | }; 8 | } 9 | 10 | return { 11 | roles: session.roles, 12 | }; 13 | } 14 | 15 | async function setRole(event, payload) { 16 | const session = Storage.get('session'); 17 | 18 | if (!session) { 19 | return { 20 | error: 'Invalid session', 21 | }; 22 | } 23 | 24 | const { 25 | index, 26 | } = payload; 27 | 28 | if (index === undefined) { 29 | return { 30 | error: 'Missing role', 31 | }; 32 | } 33 | 34 | const role = session.roles[index]; 35 | 36 | session.showRole = true; 37 | session.roleArn = role.roleArn; 38 | session.roleName = role.roleName; 39 | session.principalArn = role.principalArn; 40 | session.accountId = role.accountId; 41 | 42 | Storage.set('session', session); 43 | Storage.set('multipleRoles', false); 44 | 45 | return { 46 | status: 'selected', 47 | }; 48 | } 49 | 50 | module.exports = { 51 | getRoles, 52 | setRole, 53 | }; 54 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2023 Opal Mitchell, Rapid7 LLC. 2 | 3 | MIT License 4 | =========== 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/main/api/auth.js: -------------------------------------------------------------------------------- 1 | const SamlStrategy = require('@node-saml/passport-saml').Strategy; 2 | 3 | class Auth { 4 | constructor(options) { 5 | this.users = Object.create(null); 6 | this.passport = require('passport'); 7 | 8 | this.passport.serializeUser((user, done) => { 9 | this.users[user.nameID] = user; 10 | 11 | return done(null, user.nameID); 12 | }); 13 | 14 | this.passport.deserializeUser((id, done) => done(null, this.users[id])); 15 | 16 | this.guard = function guard(req, res, next) { 17 | if (req.isAuthenticated()) { 18 | return next(); 19 | } 20 | return res.json({ 21 | redirect: options.entryPoint, 22 | }); 23 | }; 24 | } 25 | 26 | initialize() { 27 | return this.passport.initialize(); 28 | } 29 | 30 | session() { 31 | return this.passport.session(); 32 | } 33 | 34 | authenticate(type, options) { 35 | return this.passport.authenticate(type, options); 36 | } 37 | 38 | configure(options) { 39 | const samlCallback = (profile, done) => done(null, profile); 40 | 41 | this.passport.use(new SamlStrategy(options, samlCallback)); 42 | } 43 | } 44 | 45 | module.exports = Auth; 46 | -------------------------------------------------------------------------------- /src/main/containers/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | getMetadataUrls, 3 | setMetadataUrls, 4 | getDefaultMetadata, 5 | login, 6 | isAuthenticated, 7 | hasMultipleRoles, 8 | getProfile, 9 | deleteProfile, 10 | } = require('./configure'); 11 | 12 | const { 13 | setRole, 14 | getRoles, 15 | } = require('./select-role'); 16 | 17 | const { 18 | logout, 19 | } = require('./logout'); 20 | 21 | const { 22 | refresh, 23 | } = require('./refresh'); 24 | 25 | module.exports = { 26 | channels: { 27 | configure: { 28 | 'configure:metadataUrls:get': getMetadataUrls, 29 | 'configure:metadataUrls:set': setMetadataUrls, 30 | 'configure:defaultMetadata:get': getDefaultMetadata, 31 | 'configure:profile:delete': deleteProfile, 32 | 'configure:profile:get': getProfile, 33 | 'configure:login': login, 34 | 'configure:is-authenticated': isAuthenticated, 35 | 'configure:has-multiple-roles': hasMultipleRoles, 36 | }, 37 | 'select-role': { 38 | 'select-role:get': getRoles, 39 | 'select-role:set': setRole, 40 | }, 41 | logout: { 42 | 'logout:get': logout, 43 | }, 44 | refresh: { 45 | 'refresh:get': refresh, 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/renderer/containers/select-role/Role.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ListGroupItem } from 'reactstrap'; 4 | import styled from 'styled-components'; 5 | import { 6 | BORDER_COLOR_SCHEME_MEDIA_QUERY, 7 | } from '../../constants/styles'; 8 | 9 | const SelectRoleButton = styled(ListGroupItem)` 10 | cursor: pointer; 11 | background-color: transparent; 12 | 13 | ${BORDER_COLOR_SCHEME_MEDIA_QUERY} 14 | 15 | margin-right: 0.8em; 16 | padding-left: 0.5em; 17 | padding-right: 0.5em; 18 | `; 19 | 20 | function Role(props) { 21 | const { 22 | displayAccountId, 23 | name, 24 | accountId, 25 | onClick, 26 | } = props; 27 | 28 | const displayName = displayAccountId ? `${accountId}:${name}` : name; 29 | 30 | return ( 31 | 36 | {displayName} 37 | 38 | ); 39 | } 40 | 41 | Role.propTypes = { 42 | accountId: PropTypes.string.isRequired, 43 | displayAccountId: PropTypes.bool.isRequired, 44 | name: PropTypes.string.isRequired, 45 | onClick: PropTypes.func.isRequired, 46 | }; 47 | 48 | export default Role; 49 | -------------------------------------------------------------------------------- /src/main/preload.js: -------------------------------------------------------------------------------- 1 | const { 2 | contextBridge, 3 | ipcRenderer, 4 | } = require('electron'); 5 | 6 | contextBridge.exposeInMainWorld('electronAPI', { 7 | getMetadataUrls: () => ipcRenderer.invoke('configure:metadataUrls:get'), 8 | setMetadataUrls: (args) => ipcRenderer.invoke('configure:metadataUrls:set', args), 9 | getDefaultMetadata: () => ipcRenderer.invoke('configure:defaultMetadata:get'), 10 | login: (args) => ipcRenderer.invoke('configure:login', args), 11 | isAuthenticated: () => ipcRenderer.invoke('configure:is-authenticated'), 12 | hasMultipleRoles: () => ipcRenderer.invoke('configure:has-multiple-roles'), 13 | logout: () => ipcRenderer.invoke('logout:get'), 14 | getRoles: () => ipcRenderer.invoke('select-role:get'), 15 | setRole: (args) => ipcRenderer.invoke('select-role:set', args), 16 | deleteProfile: (args) => ipcRenderer.invoke('configure:profile:delete', args), 17 | getProfile: (args) => ipcRenderer.invoke('configure:profile:get', args), 18 | refresh: () => ipcRenderer.invoke('refresh:get'), 19 | getDarkMode: () => ipcRenderer.invoke('dark-mode:get'), 20 | darkModeUpdated: (callback) => ipcRenderer.on('dark-mode:updated', callback), 21 | copy: (args) => ipcRenderer.invoke('copy', args), 22 | reloadUi: (callback) => ipcRenderer.on('reloadUi', callback), 23 | }); 24 | -------------------------------------------------------------------------------- /src/main/api/server-config.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | const express = require('express'); 3 | const morgan = require('morgan'); 4 | const bodyParser = require('body-parser'); 5 | const expressSession = require('express-session'); 6 | 7 | const app = express(); 8 | 9 | module.exports = (auth, config, secret) => { 10 | app.set('host', config.server.host); 11 | app.set('port', config.server.port); 12 | app.set('baseUrl', `http://${config.server.host}:${config.server.port}/`); 13 | app.set('configureUrlRoute', 'configure'); 14 | app.set('refreshUrlRoute', 'refresh'); 15 | app.use(morgan('[:date[iso]] :method :url :status :response-time ms - :res[content-length]')); 16 | app.use(bodyParser.json()); 17 | app.use(bodyParser.urlencoded({ extended: true })); 18 | app.use(expressSession({ 19 | resave: false, 20 | saveUninitialized: true, 21 | secret, 22 | })); 23 | app.use(auth.initialize()); 24 | app.use(auth.session()); 25 | app.use(express.static(path.join(__dirname, '..', 'build'))); 26 | 27 | if (process.env.NODE_ENV === 'development') { 28 | app.use((req, res, next) => { 29 | res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); 30 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 31 | res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE'); 32 | next(); 33 | }); 34 | } 35 | 36 | return app; 37 | }; 38 | -------------------------------------------------------------------------------- /src/renderer/constants/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const BORDER_COLOR_SCHEME_MEDIA_QUERY = ` 4 | @media (prefers-color-scheme: dark) { 5 | border: 1px solid rgb(249, 249, 249); 6 | color: rgb(249, 249, 249); 7 | } 8 | 9 | @media (prefers-color-scheme: light) { 10 | border: 1px solid rgb(108, 117, 125); 11 | color: rgb(51, 51, 51); 12 | } 13 | `; 14 | 15 | export const RoundedWrapper = styled.div` 16 | border-radius: 6px; 17 | padding: 30px; 18 | box-shadow: rgba(175, 175, 175, 0.2) 0 1px 2px 0; 19 | min-width: 100%; 20 | 21 | ${BORDER_COLOR_SCHEME_MEDIA_QUERY} 22 | `; 23 | 24 | export const RoundedContent = styled.div` 25 | border-radius: 6px; 26 | padding: 20px; 27 | word-wrap: break-word; 28 | 29 | ${BORDER_COLOR_SCHEME_MEDIA_QUERY} 30 | `; 31 | 32 | export const BUTTON_MARGIN = ` 33 | margin-left: 10px; 34 | `; 35 | 36 | export const DARK_MODE_AWARE_BORDERLESS_BUTTON = ` 37 | border: 0; 38 | margin-bottom: 3px; 39 | text-decoration: none; 40 | 41 | :hover { 42 | @media (prefers-color-scheme: dark) { 43 | color: rgb(249, 249, 249); 44 | } 45 | 46 | @media (prefers-color-scheme: light) { 47 | color: rgb(51, 51, 51); 48 | } 49 | } 50 | 51 | @media (prefers-color-scheme: dark) { 52 | color: rgb(249, 249, 249); 53 | } 54 | 55 | @media (prefers-color-scheme: light) { 56 | color: rgb(51, 51, 51); 57 | } 58 | `; 59 | -------------------------------------------------------------------------------- /src/main/api/storage.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | 3 | class Storage { 4 | /** 5 | * Constructor 6 | * @param {string} file 7 | */ 8 | constructor(file) { 9 | this.data = null; 10 | this.file = file; 11 | } 12 | 13 | /** 14 | * Load the storage file 15 | */ 16 | load() { 17 | if (this.data !== null) { 18 | return; 19 | } 20 | if (!fs.existsSync(this.file)) { 21 | this.data = {}; 22 | 23 | return; 24 | } 25 | const json = fs.readFileSync(this.file, 'utf8'); 26 | 27 | if (json === '') { 28 | this.data = {}; 29 | 30 | return; 31 | } 32 | this.data = JSON.parse(json); 33 | } 34 | 35 | /** 36 | * Save the storage file 37 | */ 38 | save() { 39 | if (this.data !== null) { 40 | fs.writeFileSync(this.file, JSON.stringify(this.data), 'utf8'); 41 | } 42 | } 43 | 44 | /** 45 | * Set the value specified by key 46 | * @param {string} key 47 | * @param {*} value 48 | */ 49 | set(key, value) { 50 | this.load(); 51 | this.data[key] = value; 52 | this.save(); 53 | } 54 | 55 | /** 56 | * Get the value by key 57 | * @param {string} key 58 | * @returns {*} 59 | */ 60 | get(key) { 61 | let value = null; 62 | 63 | this.load(); 64 | if (key in this.data) { 65 | value = this.data[key]; 66 | } 67 | 68 | return value; 69 | } 70 | 71 | delete(key) { 72 | this.load(); 73 | if (key in this.data) { 74 | delete this.data[key]; 75 | } 76 | this.save(); 77 | } 78 | } 79 | 80 | module.exports = (path) => new Storage(path); 81 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Rapid7 - Awsaml 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | settings: { 6 | react: { 7 | version: 'detect', 8 | }, 9 | 'import/core-modules': [ 10 | 'electron', 11 | 'electron-packager', 12 | 'electron-devtools-installer', 13 | ], 14 | }, 15 | extends: [ 16 | 'airbnb', 17 | ], 18 | globals: { 19 | require: true, 20 | process: true, 21 | __dirname: true, 22 | console: true, 23 | Storage: true, 24 | Manager: true, 25 | }, 26 | parser: '@babel/eslint-parser', 27 | parserOptions: { 28 | ecmaVersion: 2020, 29 | requireConfigFile: false, 30 | babelOptions: { 31 | presets: ['@babel/preset-react'], 32 | }, 33 | }, 34 | rules: { 35 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }], 36 | 'import/no-extraneous-dependencies': ['error', { 37 | devDependencies: true, 38 | }], 39 | 'linebreak-style': ['error', process.platform === 'win32' ? 'windows' : 'unix'], 40 | 'global-require': 0, 41 | }, 42 | overrides: [ 43 | { 44 | files: 'api/**/*.js', 45 | extends: ['plugin:node/recommended'], 46 | }, 47 | { 48 | files: 'test/**/*.js', 49 | env: { 50 | 'jest/globals': true, 51 | }, 52 | plugins: ['jest'], 53 | parserOptions: { 54 | sourceType: 'module', 55 | }, 56 | rules: { 57 | 'func-names': 0, 58 | 'prefer-arrow-callback': 0, 59 | 'max-nested-callbacks': 0, 60 | 'space-before-function-paren': 0, 61 | }, 62 | }, 63 | { 64 | files: 'src/**/*.js', 65 | env: { 66 | browser: true, 67 | }, 68 | plugins: [ 69 | 'react-hooks', 70 | ], 71 | }, 72 | ], 73 | }; 74 | -------------------------------------------------------------------------------- /src/main/touchbar.js: -------------------------------------------------------------------------------- 1 | const { 2 | TouchBar, 3 | } = require('electron'); 4 | const path = require('node:path'); 5 | const { app } = require('./api/server'); 6 | 7 | const { 8 | TouchBarButton, 9 | TouchBarGroup, 10 | TouchBarPopover, 11 | TouchBarSpacer, 12 | } = TouchBar; 13 | 14 | const baseUrl = process.env.ELECTRON_START_URL || app.get('baseUrl'); 15 | const configureUrl = path.join(baseUrl, app.get('configureUrlRoute')); 16 | const refreshUrl = path.join(baseUrl, app.get('refreshUrlRoute')); 17 | 18 | const buttonForProfileWithUrl = (browserWindow, profile, url) => new TouchBarButton({ 19 | backgroundColor: '#3B86CE', 20 | click: () => { 21 | browserWindow.loadURL(configureUrl, { 22 | extraHeaders: 'Content-Type: application/x-www-form-urlencoded', 23 | postData: [{ 24 | bytes: Buffer.from(`metadataUrl=${url}&origin=electron`), 25 | type: 'rawData', 26 | }], 27 | 28 | }); 29 | }, 30 | label: profile.replace(/^awsaml-/, ''), 31 | }); 32 | 33 | const loadTouchBar = (browserWindow, storedMetadataUrls) => { 34 | const refreshButton = new TouchBarButton({ 35 | backgroundColor: '#62ac5b', 36 | click: () => { 37 | browserWindow.loadURL(refreshUrl); 38 | }, 39 | label: '🔄', 40 | }); 41 | 42 | const profileButtons = storedMetadataUrls 43 | .map((storedMetadataUrl) => ( 44 | buttonForProfileWithUrl(browserWindow, storedMetadataUrl.name, storedMetadataUrl.url) 45 | )); 46 | const touchbar = new TouchBar({ 47 | items: [ 48 | refreshButton, 49 | new TouchBarGroup({ 50 | items: profileButtons.slice(0, 3), 51 | }), 52 | new TouchBarSpacer({ 53 | size: 'flexible', 54 | }), 55 | new TouchBarPopover({ 56 | items: profileButtons, 57 | label: '👥 More Profiles', 58 | }), 59 | ], 60 | }); 61 | 62 | browserWindow.setTouchBar(touchbar); 63 | }; 64 | 65 | module.exports = { 66 | loadTouchBar, 67 | }; 68 | -------------------------------------------------------------------------------- /src/renderer/containers/configure/LoginList.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { ListGroup } from 'reactstrap'; 5 | import update from 'immutability-helper'; 6 | 7 | import Login from './Login'; 8 | 9 | const ScrollableListGroup = styled(ListGroup)` 10 | overflow-x: hidden; 11 | height: 300px; 12 | `; 13 | 14 | function LoginList(props) { 15 | const { 16 | filteredMetadataUrls, 17 | deleteCallback, 18 | reOrderCallback, 19 | errorHandler, 20 | darkMode, 21 | } = props; 22 | 23 | const moveLogin = useCallback((dragIndex, hoverIndex) => { 24 | if (dragIndex === undefined) { 25 | return; 26 | } 27 | 28 | const updatedMetadataUrls = update(filteredMetadataUrls, { 29 | $splice: [ 30 | [dragIndex, 1], 31 | [hoverIndex, 0, filteredMetadataUrls[dragIndex]], 32 | ], 33 | }); 34 | 35 | reOrderCallback(updatedMetadataUrls); 36 | }, [filteredMetadataUrls]); 37 | 38 | return ( 39 | 40 | {filteredMetadataUrls.map(({ url, name, profileUuid }, index) => ( 41 | 52 | ))} 53 | 54 | ); 55 | } 56 | 57 | LoginList.propTypes = { 58 | filteredMetadataUrls: PropTypes.arrayOf(PropTypes.shape({ 59 | url: PropTypes.string.isRequired, 60 | name: PropTypes.string.isRequired, 61 | profileUuid: PropTypes.string.isRequired, 62 | })).isRequired, 63 | deleteCallback: PropTypes.func.isRequired, 64 | reOrderCallback: PropTypes.func.isRequired, 65 | errorHandler: PropTypes.func.isRequired, 66 | darkMode: PropTypes.bool.isRequired, 67 | }; 68 | 69 | export default LoginList; 70 | -------------------------------------------------------------------------------- /src/main/protocol.js: -------------------------------------------------------------------------------- 1 | const { 2 | protocol, 3 | session, 4 | } = require('electron'); 5 | const { 6 | readFileSync, 7 | } = require('node:fs'); 8 | const url = require('node:url'); 9 | const { refreshJit } = require('./containers/refresh-jit'); 10 | 11 | function registerSchemas() { 12 | protocol.registerSchemesAsPrivileged([{ 13 | scheme: 'jit', 14 | privileges: { supportFetchAPI: true }, 15 | }]); 16 | } 17 | 18 | function registerHandlers() { 19 | protocol.handle('awsaml', (request) => { 20 | const prefix = 'awsaml://'.length; 21 | 22 | return new Response(readFileSync(url.fileURLToPath(`file://${request.url.slice(prefix)}`))); 23 | }); 24 | 25 | protocol.handle('jit', async (request) => { 26 | const { host, pathname } = new URL(request.url); 27 | if (host === 'get-active-profiles') { 28 | const activeProfiles = Object.values(Manager.reloaders).map((r) => r.role); 29 | return new Response(JSON.stringify({ activeProfiles })); 30 | } 31 | 32 | const sessionId = await session.defaultSession.cookies.get({ name: 'session_id', domain: host }); 33 | const body = await request.body.getReader().read(); 34 | const reqBody = JSON.parse(Buffer.from(body.value).toString()); 35 | const profile = { 36 | ...reqBody, 37 | roleName: reqBody.roleArn.split('/')[1], 38 | header: { 39 | 'X-Auth-Token': sessionId[0].value, 40 | }, 41 | apiUri: `https://${host}${pathname}`, 42 | showRole: false, 43 | }; 44 | 45 | let data; 46 | try { 47 | data = await refreshJit(profile); 48 | } catch (err) { 49 | const errBody = JSON.stringify({ 50 | error_message: err?.message || 'unknown', 51 | error_type: err?.error_type || 'unknown', 52 | }); 53 | return new Response(errBody, { status: 500 }); 54 | } 55 | 56 | const activeProfiles = Object.values(Manager.reloaders).map((r) => r.role); 57 | data.activeProfiles = activeProfiles; 58 | return new Response(JSON.stringify(data)); 59 | }); 60 | } 61 | 62 | module.exports = { 63 | registerHandlers, 64 | registerSchemas, 65 | }; 66 | -------------------------------------------------------------------------------- /src/renderer/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Route, 4 | Routes, 5 | MemoryRouter, 6 | } from 'react-router-dom'; 7 | import { createGlobalStyle } from 'styled-components'; 8 | import 'prismjs'; 9 | import 'prismjs/themes/prism-tomorrow.css'; 10 | import Configure from './configure/Configure'; 11 | import Refresh from './refresh/Refresh'; 12 | import SelectRole from './select-role/SelectRole'; 13 | import ErrorBoundary from './ErrorBoundary'; 14 | import DebugRoute from './components/DebugRoute'; 15 | 16 | const debug = process.env.NODE_ENV === 'development'; 17 | const GlobalStyle = createGlobalStyle` 18 | @media (prefers-color-scheme: dark) { 19 | body { 20 | background: #333; 21 | color: rgb(249, 249, 249); 22 | } 23 | } 24 | 25 | @media (prefers-color-scheme: light) { 26 | body { 27 | background: rgb(249, 249, 249); 28 | color: #333; 29 | } 30 | } 31 | 32 | html { 33 | height: 100%; 34 | } 35 | 36 | body { 37 | margin: 0; 38 | padding: 0; 39 | font-family: sans-serif; 40 | min-height: 100%; 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | body > * { 46 | flex-grow: 1; 47 | } 48 | 49 | summary { 50 | padding: 0.25rem; 51 | display: flex; 52 | width: 100%; 53 | align-items: center; 54 | } 55 | 56 | dd { 57 | margin-bottom: 10px; 58 | } 59 | 60 | input[type="text"], input[type="url"] { 61 | border: 1px solid #6c757d; 62 | } 63 | .has-error .form-control, :invalid input { 64 | border: 2px solid red; 65 | } 66 | `; 67 | 68 | function App() { 69 | return ( 70 | 71 | 72 | 73 | 74 | } path="/" exact /> 75 | } path="/refresh" exact /> 76 | } path="/select-role" exact /> 77 | {debug ? } /> : ''} 78 | 79 | 80 | 81 | 82 | 83 | ); 84 | } 85 | 86 | export default App; 87 | -------------------------------------------------------------------------------- /src/renderer/containers/configure/Configure.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | import { 5 | Container, 6 | Row, 7 | } from 'reactstrap'; 8 | import Logo from '../components/Logo'; 9 | import RecentLogins from './RecentLogins'; 10 | import ConfigureMetadata from './ConfigureMetadata'; 11 | import { 12 | RoundedContent, 13 | RoundedWrapper, 14 | } from '../../constants/styles'; 15 | import Error from '../components/Error'; 16 | 17 | const CenteredDivColumn = styled.div` 18 | float: none; 19 | margin: 0 auto; 20 | `; 21 | 22 | const RoundedCenteredDivColumnContent = styled(RoundedContent)(CenteredDivColumn); 23 | const RoundedCenteredDivColumnWrapper = styled(RoundedWrapper)(CenteredDivColumn); 24 | 25 | function Configure() { 26 | const [auth, setAuth] = useState(false); 27 | const [selectRole, setSelectRole] = useState(false); 28 | 29 | const [metadataUrlValid, setMetadataUrlValid] = useState(true); 30 | const [error, setError] = useState(''); 31 | 32 | useEffect(() => { 33 | (async () => { 34 | const isAuthenticated = await window.electronAPI.isAuthenticated(); 35 | setAuth(isAuthenticated); 36 | 37 | const mustSelectRole = await window.electronAPI.hasMultipleRoles(); 38 | setSelectRole(mustSelectRole); 39 | })(); 40 | 41 | return () => {}; 42 | }); 43 | const errorHandler = (err) => { 44 | console.error(err); // eslint-disable-line no-console 45 | setError(err); 46 | }; 47 | 48 | if (auth) { 49 | return ; 50 | } 51 | 52 | if (selectRole) { 53 | return ; 54 | } 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 70 | 71 | 72 | 73 | 74 | ); 75 | } 76 | 77 | export default Configure; 78 | -------------------------------------------------------------------------------- /src/renderer/containers/components/InputGroupWithCopyButton.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Button, 5 | InputGroup, 6 | Input, 7 | Tooltip, 8 | } from 'reactstrap'; 9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 10 | 11 | function InputGroupWithCopyButton(props) { 12 | const { 13 | id: idFromProps, 14 | className, 15 | inputClassName, 16 | name, 17 | value, 18 | message, 19 | multiLine, 20 | darkMode, 21 | } = props; 22 | const [tooltipState, setTooltipState] = useState(false); 23 | 24 | const handleTooltipTargetClick = async () => { 25 | setTooltipState(true); 26 | await window.electronAPI.copy(value); 27 | 28 | setTimeout(function () { // eslint-disable-line prefer-arrow-callback, func-names 29 | setTooltipState(false); 30 | }, 3000); 31 | }; 32 | 33 | const id = `icon-${idFromProps}`; 34 | 35 | return ( 36 | 37 | 45 | 62 | 63 | ); 64 | } 65 | 66 | InputGroupWithCopyButton.propTypes = { 67 | className: PropTypes.string, 68 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 69 | inputClassName: PropTypes.string, 70 | message: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 71 | multiLine: PropTypes.bool, 72 | name: PropTypes.string.isRequired, 73 | value: PropTypes.string.isRequired, 74 | darkMode: PropTypes.bool, 75 | }; 76 | 77 | InputGroupWithCopyButton.defaultProps = { 78 | message: 'Copied!', 79 | multiLine: false, 80 | className: '', 81 | inputClassName: '', 82 | darkMode: false, 83 | }; 84 | 85 | export default InputGroupWithCopyButton; 86 | -------------------------------------------------------------------------------- /src/main/containers/auth.js: -------------------------------------------------------------------------------- 1 | const url = require('node:url'); 2 | 3 | function authHandler(app) { 4 | return async (req, res) => { 5 | let roleAttr = req.user['https://aws.amazon.com/SAML/Attributes/Role']; 6 | let frontend = app.get('baseUrl'); 7 | 8 | frontend = new url.URL(frontend); 9 | 10 | // Convert roleAttr to an array if it isn't already one 11 | if (!Array.isArray(roleAttr)) { 12 | roleAttr = [roleAttr]; 13 | } 14 | 15 | const roles = roleAttr.map((arns, i) => { 16 | const [roleArn, principalArn] = arns.split(','); 17 | const roleArnSegments = roleArn.split(':'); 18 | const accountId = roleArnSegments[4]; 19 | const roleName = roleArnSegments[5].replace('role/', ''); 20 | 21 | return { 22 | accountId, 23 | index: i, 24 | principalArn, 25 | roleArn, 26 | roleName, 27 | }; 28 | }); 29 | 30 | const session = req.session.passport; 31 | 32 | session.samlResponse = req.body.SAMLResponse; 33 | session.roles = roles; 34 | 35 | if (roles.length > 1) { 36 | // If the session has a previous role, see if it matches 37 | // the latest roles from the current SAML assertion. If it 38 | // doesn't match, wipe it from the session. 39 | if (session.roleArn && session.principalArn) { 40 | const found = roles 41 | // eslint-disable-next-line max-len 42 | .find((role) => role.roleArn === session.roleArn && role.principalArn === session.principalArn); 43 | 44 | if (!found) { 45 | session.showRole = undefined; 46 | session.roleArn = undefined; 47 | session.roleName = undefined; 48 | session.principalArn = undefined; 49 | session.accountId = undefined; 50 | } 51 | } 52 | 53 | // If the session still has a previous role, proceed directly to auth. 54 | // Otherwise ask the user to select a role. 55 | if (session.roleArn && session.principalArn && session.roleName && session.accountId) { 56 | Storage.set('authenticated', true); 57 | } else { 58 | Storage.set('multipleRoles', true); 59 | } 60 | } else { 61 | const role = roles[0]; 62 | 63 | Storage.set('authenticated', true); 64 | 65 | session.showRole = false; 66 | session.roleArn = role.roleArn; 67 | session.roleName = role.roleName; 68 | session.principalArn = role.principalArn; 69 | session.accountId = role.accountId; 70 | } 71 | 72 | Storage.set('session', session); 73 | 74 | res.redirect(frontend); 75 | }; 76 | } 77 | 78 | module.exports = { 79 | authHandler, 80 | }; 81 | -------------------------------------------------------------------------------- /src/main/containers/refresh-jit.js: -------------------------------------------------------------------------------- 1 | const AwsCredentials = require('../api/aws-credentials'); 2 | const ResponseObj = require('../api/response'); 3 | const Reloader = require('../api/reloader/reloader'); 4 | 5 | const credentials = new AwsCredentials(); 6 | 7 | async function refreshJitCallback(profileName, session) { 8 | const refreshResponseObj = { 9 | ...ResponseObj, 10 | accountId: session.accountId, 11 | roleName: session.roleName, 12 | showRole: session.showRole, 13 | profileName, 14 | }; 15 | 16 | let creds = {}; 17 | let response; 18 | try { 19 | response = await fetch(encodeURI(session.apiUri), { 20 | method: 'GET', 21 | headers: session.header, 22 | }); 23 | } catch (err) { 24 | console.error(err); // eslint-disable-line no-console 25 | Manager.removeByName(profileName); 26 | throw new Error(`AWSAML is unable to fetch credentials from ICS. HTTPS request to URI: ${session.apiUri}`); 27 | } 28 | 29 | if (response.ok) { 30 | creds = await response.json(); 31 | } else { 32 | Manager.removeByName(profileName); 33 | throw new Error('An error occurred while fetching credentials from ICS'); 34 | } 35 | 36 | const credentialResponseObj = { 37 | ...refreshResponseObj, 38 | accessKey: creds.AccessKeyId, 39 | secretKey: creds.SecretAccessKey, 40 | sessionToken: creds.SessionToken, 41 | expiration: creds.Expiration, 42 | }; 43 | 44 | try { 45 | credentials.saveSync(creds, profileName, session.region); 46 | } catch (e) { 47 | return { 48 | ...credentialResponseObj, 49 | error: e, 50 | }; 51 | } 52 | 53 | return credentialResponseObj; 54 | } 55 | 56 | async function refreshJit(session) { 57 | const profileName = `awsaml-${session.accountId}`; 58 | let r = Manager.get(profileName); 59 | 60 | if (!r) { 61 | r = new Reloader({ 62 | name: profileName, 63 | async callback() { 64 | await refreshJitCallback(profileName, session).catch((e) => { throw e; }); 65 | }, 66 | interval: (session.duration / 2) * 1000, 67 | role: session.roleConfigId, 68 | }); 69 | Manager.add(r); 70 | r.start(); 71 | } else { 72 | if (session.roleConfigId !== r.role) { 73 | r.role = session.roleConfigId; 74 | r.setCallback( 75 | async () => { 76 | await refreshJitCallback(profileName, session).catch((e) => { throw e; }); 77 | }, 78 | ); 79 | r.role = session.roleConfigId; 80 | } 81 | r.restart(); 82 | } 83 | return refreshJitCallback(profileName, session).catch((e) => { throw e; }); 84 | } 85 | 86 | module.exports = { 87 | refreshJit, 88 | }; 89 | -------------------------------------------------------------------------------- /src/main/api/aws-credentials.js: -------------------------------------------------------------------------------- 1 | const ini = require('ini'); 2 | const fs = require('node:fs'); 3 | const path = require('node:path'); 4 | 5 | class AwsCredentials { 6 | save(credentials, profile, done, region) { 7 | this.saveAsIniFile(credentials, profile, done, region); 8 | } 9 | 10 | saveSync(credentials, profile, region) { 11 | const done = (err) => { 12 | if (err) { 13 | throw err; 14 | } 15 | }; 16 | this.saveAsIniFile(credentials, profile, done, region); 17 | } 18 | 19 | // eslint-disable-next-line class-methods-use-this, consistent-return 20 | saveAsIniFile(credentials, profile, done, region = '') { 21 | const home = AwsCredentials.resolveHomePath(); 22 | 23 | if (!home) { 24 | return done(new Error('Cannot save AWS credentials, HOME path not set')); 25 | } 26 | 27 | const configFile = path.join(home, '.aws', 'credentials'); 28 | 29 | if (!credentials) { 30 | return done(new Error('Invalid AWS credentials')); 31 | } 32 | 33 | if (!profile) { 34 | return done(new Error('Cannot save AWS credentials, profile not set')); 35 | } 36 | 37 | try { 38 | fs.mkdirSync(path.join(home, '.aws'), { 39 | recursive: true, 40 | mode: '0700', 41 | }); 42 | } catch (e) { 43 | return done(e); 44 | } 45 | 46 | let data; 47 | try { 48 | data = fs.readFileSync(configFile, { 49 | encoding: 'utf8', 50 | }); 51 | } catch (error) { 52 | if (error.code !== 'ENOENT') { 53 | return done(error); 54 | } 55 | } 56 | 57 | let config = Object.create(null); 58 | 59 | if (data && data !== '') { 60 | config = ini.parse(data); 61 | } 62 | 63 | config[profile] = { 64 | aws_access_key_id: credentials.AccessKeyId, 65 | aws_secret_access_key: credentials.SecretAccessKey, 66 | aws_session_token: credentials.SessionToken, 67 | // Some libraries e.g. boto v2.38.0, expect an "aws_security_token" entry. 68 | aws_security_token: credentials.SessionToken, 69 | }; 70 | 71 | // Include expiration if it is available 72 | if (credentials.Expiration) { 73 | config[profile].expiration = credentials.Expiration; 74 | } 75 | 76 | if (region.includes('gov')) { 77 | config[profile].region = region; 78 | } 79 | 80 | config = ini.encode(config, { 81 | whitespace: true, 82 | }); 83 | 84 | fs.writeFile(configFile, config, 'utf8', done); 85 | } 86 | 87 | static resolveHomePath() { 88 | const { 89 | env, 90 | } = process; 91 | 92 | return env.HOME 93 | || env.USERPROFILE 94 | || (env.HOMEPATH ? ((env.HOMEDRIVE || 'C:/') + env.HOMEPATH) : null); 95 | } 96 | } 97 | 98 | module.exports = AwsCredentials; 99 | -------------------------------------------------------------------------------- /src/renderer/containers/select-role/SelectRole.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Container, 4 | ListGroup, 5 | Row, 6 | } from 'reactstrap'; 7 | import { Navigate } from 'react-router-dom'; 8 | import styled from 'styled-components'; 9 | import Error from '../components/Error'; 10 | import Role from './Role'; 11 | import Logo from '../components/Logo'; 12 | import { 13 | RoundedContent, 14 | RoundedWrapper, 15 | } from '../../constants/styles'; 16 | 17 | const SelectRoleHeader = styled.h4` 18 | margin-top: 15px; 19 | padding-top: 15px; 20 | `; 21 | 22 | function SelectRole() { 23 | const [displayAccountId, setDisplayAccountId] = useState(true); 24 | const [roles, setRoles] = useState([]); 25 | const [status, setStatus] = useState(''); 26 | const [error, setError] = useState(''); 27 | const [darkMode, setDarkMode] = useState(false); 28 | 29 | useEffect(() => { 30 | (async () => { 31 | const data = await window.electronAPI.getRoles(); 32 | setRoles(data.roles); 33 | 34 | const uniqueAccountIds = new Set(data.roles.map((role) => role.accountId)); 35 | 36 | if (uniqueAccountIds.size === 1) { 37 | setDisplayAccountId(false); 38 | } 39 | })(); 40 | 41 | window.electronAPI.darkModeUpdated((event, value) => { 42 | setDarkMode(value); 43 | }); 44 | 45 | return () => {}; 46 | }, []); 47 | 48 | const handleClick = (index) => async (event) => { 49 | event.preventDefault(); 50 | 51 | const data = await window.electronAPI.setRole({ index }); 52 | 53 | if (data.error) { 54 | setError(data.error); 55 | } else { 56 | setStatus(data.status); 57 | } 58 | }; 59 | 60 | if (status === 'selected') { 61 | return ; 62 | } 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 | 70 | 71 | Select a role: 72 | 73 | { 74 | roles.map((role) => { 75 | const roleOnClick = handleClick(role.index); 76 | 77 | return ( 78 | 89 | ); 90 | }) 91 | } 92 | 93 | 94 | 95 | 96 | 97 | ); 98 | } 99 | 100 | export default SelectRole; 101 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at security@rapid7.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/renderer/containers/configure/ConfigureMetadata.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Button, 5 | Input, 6 | } from 'reactstrap'; 7 | import styled from 'styled-components'; 8 | 9 | const FullSizeLabel = styled.label` 10 | width: 100%; 11 | padding-bottom: 1rem; 12 | `; 13 | 14 | function ConfigureMetadata(props) { 15 | const { 16 | setError, 17 | setMetadataUrlValid, 18 | } = props; 19 | 20 | const [metadataUrl, setMetadataUrl] = useState(''); 21 | const [profileName, setProfileName] = useState(''); 22 | const [urlGroupClass, setUrlGroupClass] = useState('form-group'); 23 | const [darkMode, setDarkMode] = useState(false); 24 | 25 | useEffect(() => { 26 | (async () => { 27 | const { 28 | url, 29 | name, 30 | } = await window.electronAPI.getDefaultMetadata(); 31 | setMetadataUrl(url); 32 | setProfileName(name); 33 | 34 | const dm = await window.electronAPI.getDarkMode(); 35 | setDarkMode(dm); 36 | })(); 37 | 38 | window.electronAPI.darkModeUpdated((event, value) => { 39 | setDarkMode(value); 40 | }); 41 | }, []); 42 | 43 | const handleInputChange = ({ target: { name, value } }) => { 44 | switch (name) { 45 | case 'profileName': 46 | setProfileName(value); 47 | break; 48 | case 'metadataUrl': 49 | setMetadataUrl(value); 50 | break; 51 | default: 52 | break; 53 | } 54 | }; 55 | 56 | const handleSubmit = async (event) => { 57 | event.preventDefault(); 58 | 59 | const payload = { 60 | metadataUrl, 61 | profileName, 62 | }; 63 | 64 | const { 65 | error, 66 | redirect, 67 | metadataUrlValid, 68 | } = await window.electronAPI.login(payload); 69 | if (error) { 70 | setError(error); 71 | setMetadataUrlValid(metadataUrlValid); 72 | setUrlGroupClass('form-group has-error'); 73 | } 74 | 75 | if (redirect) { 76 | document.location.replace(redirect); 77 | } 78 | }; 79 | 80 | const handleKeyDown = (event) => event.keyCode === 13 && handleSubmit(event); 81 | 82 | return ( 83 |
84 | Configure 85 |
86 | 87 | SAML Metadata URL 88 | 99 | 100 |
101 |
102 | 103 | Account Alias 104 | 114 | 115 |
116 | 123 |
124 | ); 125 | } 126 | 127 | ConfigureMetadata.propTypes = { 128 | setError: PropTypes.func.isRequired, 129 | setMetadataUrlValid: PropTypes.func.isRequired, 130 | }; 131 | 132 | export default ConfigureMetadata; 133 | -------------------------------------------------------------------------------- /src/main/containers/refresh.js: -------------------------------------------------------------------------------- 1 | const { STSClient, AssumeRoleWithSAMLCommand } = require('@aws-sdk/client-sts'); 2 | const { webContents } = require('electron'); 3 | const config = require('../api/config.json'); 4 | const AwsCredentials = require('../api/aws-credentials'); 5 | const ResponseObj = require('../api/response'); 6 | const Reloader = require('../api/reloader/reloader'); 7 | const { 8 | app, 9 | } = require('../api/server'); 10 | 11 | const credentials = new AwsCredentials(); 12 | 13 | async function refreshCallback(profileName, session, wc) { 14 | const refreshResponseObj = { 15 | ...ResponseObj, 16 | accountId: session.accountId, 17 | roleName: session.roleName, 18 | showRole: session.showRole, 19 | }; 20 | 21 | const region = session.roleArn.includes('aws-us-gov') ? 'us-gov-west-1' : 'us-east-1'; 22 | const client = new STSClient({ region }); 23 | 24 | const input = { 25 | DurationSeconds: config.aws.duration, 26 | PrincipalArn: session.principalArn, 27 | RoleArn: session.roleArn, 28 | SAMLAssertion: session.samlResponse, 29 | }; 30 | 31 | let data; 32 | const command = new AssumeRoleWithSAMLCommand(input); 33 | try { 34 | data = await client.send(command); 35 | } catch (e) { 36 | console.error(e); // eslint-disable-line no-console 37 | return { 38 | redirect: config.auth.entryPoint, 39 | logout: true, 40 | }; 41 | } 42 | 43 | const credentialResponseObj = { 44 | ...refreshResponseObj, 45 | accessKey: data.Credentials.AccessKeyId, 46 | secretKey: data.Credentials.SecretAccessKey, 47 | sessionToken: data.Credentials.SessionToken, 48 | expiration: data.Credentials.Expiration, 49 | }; 50 | 51 | const metadataUrl = app.get('metadataUrl'); 52 | 53 | // Update the stored profile with account number(s) and profile names 54 | const metadataUrls = (Storage.get('metadataUrls') || []).map((metadata) => { 55 | const ret = { 56 | ...metadata, 57 | }; 58 | 59 | if (metadata.url === metadataUrl) { 60 | // If the stored metadataUrl label value is the same as the URL 61 | // default to the profile name! 62 | if (metadata.name === metadataUrl) { 63 | ret.name = profileName; 64 | } 65 | ret.roles = session.roles.map((role) => role.roleArn); 66 | } 67 | 68 | return ret; 69 | }); 70 | 71 | Storage.set('metadataUrls', metadataUrls); 72 | 73 | // Fetch the metadata profile name for this URL 74 | const profile = metadataUrls.find((metadata) => metadata.url === metadataUrl); 75 | credentialResponseObj.profileName = profile.name; 76 | 77 | try { 78 | credentials.saveSync(data.Credentials, profileName, region); 79 | } catch (e) { 80 | return { 81 | ...credentialResponseObj, 82 | error: e, 83 | }; 84 | } 85 | 86 | wc.send('reloadUi', credentialResponseObj); 87 | return credentialResponseObj; 88 | } 89 | 90 | async function refresh() { 91 | const session = Storage.get('session'); 92 | const wc = webContents.getFocusedWebContents(); 93 | 94 | if (session === undefined) { 95 | return { 96 | error: 'Invalid session', 97 | logout: true, 98 | }; 99 | } 100 | const profileName = `awsaml-${session.accountId}`; 101 | 102 | let r = Manager.get(profileName); 103 | if (!r) { 104 | r = new Reloader({ 105 | name: profileName, 106 | async callback() { 107 | await refreshCallback(profileName, session, wc); 108 | }, 109 | interval: (config.aws.duration / 2) * 1000, 110 | }); 111 | Manager.add(r); 112 | r.start(); 113 | } else { 114 | r.restart(); 115 | } 116 | return refreshCallback(profileName, session, wc); 117 | } 118 | 119 | module.exports = { 120 | refresh, 121 | }; 122 | -------------------------------------------------------------------------------- /src/renderer/containers/configure/RecentLogins.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { 5 | Input, 6 | } from 'reactstrap'; 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 8 | import { DndProvider } from 'react-dnd'; 9 | import { HTML5Backend } from 'react-dnd-html5-backend'; 10 | import LoginList from './LoginList'; 11 | 12 | const RecentLoginsHeader = styled.h4` 13 | border-top: 2px solid rgb(203, 203, 203); 14 | margin-top: 15px; 15 | padding-top: 15px; 16 | `; 17 | 18 | const SearchContainer = styled.div` 19 | position: absolute; 20 | right: 0; 21 | top: 10px; 22 | width: 250px; 23 | `; 24 | 25 | const SearchIcon = styled(FontAwesomeIcon)` 26 | position: absolute; 27 | top: 11px; 28 | left: 10px; 29 | `; 30 | 31 | const SearchInput = styled(Input)` 32 | padding-left: 30px; 33 | `; 34 | 35 | const filterMetadataUrls = (metadataUrls, filterText) => { 36 | if (!filterText) { 37 | return metadataUrls; 38 | } 39 | 40 | const tokens = filterText.split(' ').map((token) => token.toLowerCase()); 41 | 42 | return metadataUrls.filter((metadataUrl) => (tokens.every((token) => { 43 | // Compare profile name 44 | if (metadataUrl.name.toLowerCase().indexOf(token) !== -1) { 45 | return true; 46 | } 47 | 48 | // Compare profile URL 49 | if (metadataUrl.url.toLowerCase().indexOf(token) !== -1) { 50 | return true; 51 | } 52 | 53 | // Compare profile roles 54 | return (metadataUrl.roles || []).some((role) => role.toLowerCase().indexOf(token) !== -1); 55 | }) 56 | )); 57 | }; 58 | 59 | function RecentLogins(props) { 60 | const { 61 | errorHandler, 62 | } = props; 63 | 64 | const [filterText, setFilterText] = useState(''); 65 | const [metadataUrls, setMetadataUrls] = useState(''); 66 | const [darkMode, setDarkMode] = useState(false); 67 | 68 | useEffect(() => { 69 | (async () => { 70 | const mdUrls = await window.electronAPI.getMetadataUrls(); 71 | setMetadataUrls(mdUrls); 72 | 73 | const dm = await window.electronAPI.getDarkMode(); 74 | setDarkMode(dm); 75 | })(); 76 | 77 | window.electronAPI.darkModeUpdated((event, value) => { 78 | setDarkMode(value); 79 | }); 80 | 81 | return () => {}; 82 | }, []); 83 | 84 | // eslint-disable-next-line max-len 85 | const handleFilterInputChange = ({ currentTarget: { value: ft } }) => setFilterText(ft); 86 | 87 | const deleteCallback = useCallback((deleted) => { 88 | const updatedMetadataUrls = metadataUrls 89 | .filter((metadataUrl) => metadataUrl.profileUuid !== deleted.profileUuid); 90 | setMetadataUrls(updatedMetadataUrls); 91 | }, [metadataUrls]); 92 | 93 | const reorderCallback = useCallback((updatedMetadataUrls) => { 94 | setMetadataUrls(updatedMetadataUrls); 95 | window.electronAPI.setMetadataUrls(updatedMetadataUrls); 96 | }, []); 97 | 98 | const filteredMetadataUrls = filterMetadataUrls(metadataUrls, filterText); 99 | 100 | return ( 101 |
105 | Recent Logins 106 | 107 | 108 | 112 | 113 | 114 | 121 | 122 |
123 | ); 124 | } 125 | 126 | RecentLogins.propTypes = { 127 | errorHandler: PropTypes.func.isRequired, 128 | }; 129 | 130 | export default RecentLogins; 131 | -------------------------------------------------------------------------------- /src/renderer/containers/refresh/Credentials.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Card, 4 | CardBody, 5 | Button, 6 | Collapse, 7 | } from 'reactstrap'; 8 | import PropTypes from 'prop-types'; 9 | import styled from 'styled-components'; 10 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 11 | import InputGroupWithCopyButton from '../components/InputGroupWithCopyButton'; 12 | import { 13 | DARK_MODE_AWARE_BORDERLESS_BUTTON, 14 | } from '../../constants/styles'; 15 | 16 | const CredProps = styled.dl` 17 | display: grid; 18 | grid-template-columns: auto 1fr; 19 | margin: 0; 20 | padding: .5rem; 21 | `; 22 | 23 | const CredPropsKey = styled.dt` 24 | grid-column: 1; 25 | margin-right: 1rem; 26 | margin-bottom: 10px; 27 | line-height: 2.5rem; 28 | 29 | @media (prefers-color-scheme: light) { 30 | color: #333; 31 | } 32 | 33 | @media (prefers-color-scheme: dark) { 34 | color: rgb(249, 249, 249); 35 | } 36 | `; 37 | 38 | const CredPropsVal = styled.dd` 39 | grid-column: 2; 40 | `; 41 | 42 | const SmallMarginCardBody = styled(CardBody)` 43 | padding: 1.25rem 0.5rem; 44 | `; 45 | 46 | const BorderlessButton = styled(Button)` 47 | ${DARK_MODE_AWARE_BORDERLESS_BUTTON} 48 | `; 49 | 50 | const DarkModeAwareCard = styled(Card)` 51 | @media (prefers-color-scheme: light) { 52 | background-color: rgb(249, 249, 249); 53 | border-color: #333; 54 | } 55 | 56 | @media (prefers-color-scheme: dark) { 57 | background-color: #333; 58 | border-color: rgb(249, 249, 249); 59 | } 60 | `; 61 | 62 | function Credentials(props) { 63 | const { 64 | awsAccessKey, 65 | awsSecretKey, 66 | awsSessionToken, 67 | darkMode, 68 | } = props; 69 | 70 | const [caretDirection, setCaretDirection] = useState('right'); 71 | const [isOpen, setIsOpen] = useState(false); 72 | 73 | const handleCollapse = () => { 74 | setCaretDirection(caretDirection === 'right' ? 'down' : 'right'); 75 | setIsOpen(!isOpen); 76 | }; 77 | 78 | const creds = new Map(); 79 | 80 | if (awsAccessKey) { 81 | creds.set('Access Key', awsAccessKey); 82 | } 83 | 84 | if (awsSecretKey) { 85 | creds.set('Secret Key', awsSecretKey); 86 | } 87 | 88 | if (awsSessionToken) { 89 | creds.set('Session Token', awsSessionToken); 90 | } 91 | 92 | return ( 93 |
94 | 99 | 100 | {' '} 101 | Credentials 102 | 103 | 104 | 105 | 106 | 107 | { 108 | Array.from(creds).map(([name, value]) => { 109 | const id = name.toLowerCase().split(' ').join('-'); 110 | 111 | return ([ 112 | 113 | {name} 114 | : 115 | , 116 | 117 | 123 | , 124 | ]); 125 | }) 126 | } 127 | 128 | 129 | 130 | 131 |
132 | ); 133 | } 134 | 135 | Credentials.propTypes = { 136 | awsAccessKey: PropTypes.string.isRequired, 137 | awsSecretKey: PropTypes.string.isRequired, 138 | awsSessionToken: PropTypes.string.isRequired, 139 | darkMode: PropTypes.bool.isRequired, 140 | }; 141 | 142 | export default Credentials; 143 | -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | const util = require('node:util'); 2 | const fs = require('node:fs'); 3 | const path = require('node:path'); 4 | const exec = util.promisify(require('node:child_process').exec); 5 | const { globSync } = require('glob'); 6 | const awsaml = require('./package.json'); 7 | 8 | let includeFiles = [ 9 | // we need to make sure the project root directory is included 10 | '', 11 | ...globSync('src/**'), 12 | 'LICENSE.md', 13 | 'package.json', 14 | // per electron-packager's docs, a set of files in the node_modules directory are always ignored 15 | // unless we are providing an IgnoreFunction. Because we want to ignore a lot more files than 16 | // packager does by default, we need to ensure that we're including the relevant node_modules 17 | // while ignoring what packager normally would. 18 | // See https://electron.github.io/electron-packager/main/interfaces/electronpackager.options.html#ignore. 19 | ...globSync('node_modules/**', { 20 | ignore: [ 21 | 'node_modules/.bin/**', 22 | 'node_modules/electron/**', 23 | 'node_modules/electron-prebuilt/**', 24 | 'node_modules/electron-prebuilt-compile/**', 25 | ], 26 | }), 27 | ]; 28 | 29 | const outDirName = path.join(__dirname, 'out'); 30 | const outDirStat = fs.statSync(outDirName, { 31 | throwIfNoEntry: false, 32 | }); 33 | const buildDirName = path.join(__dirname, 'build'); 34 | const buildDirStat = fs.statSync(buildDirName, { 35 | throwIfNoEntry: false, 36 | }); 37 | 38 | const config = { 39 | packagerConfig: { 40 | appBundleId: 'com.rapid7.awsaml', 41 | asar: true, 42 | helperBundleId: 'com.rapid7.awsaml.helper', 43 | prune: true, 44 | ignore: (p) => !includeFiles.includes(p.replace('/', '')), 45 | name: 'Awsaml', 46 | darwinDarkModeSupport: true, 47 | icon: 'images/icon', 48 | }, 49 | rebuildConfig: {}, 50 | hooks: { 51 | generateAssets: async () => { 52 | // Clear the build directory if it exists 53 | if (buildDirStat && buildDirStat.isDirectory()) { 54 | fs.rmSync(buildDirName, { 55 | force: true, 56 | recursive: true, 57 | }); 58 | } 59 | 60 | await exec('yarn react-build'); 61 | // Update the files we want to include with generated build artifacts 62 | includeFiles = [ 63 | ...includeFiles, 64 | ...globSync('build/**'), 65 | ]; 66 | }, 67 | prePackage: () => { 68 | // Clear the out directory if it exists 69 | if (outDirStat && outDirStat.isDirectory()) { 70 | fs.rmSync(outDirName, { 71 | force: true, 72 | recursive: true, 73 | }); 74 | } 75 | }, 76 | }, 77 | makers: [ 78 | { 79 | name: '@electron-forge/maker-squirrel', 80 | config: { 81 | authors: awsaml.contributors.join(', '), 82 | setupIcon: path.join(__dirname, 'images', 'icon.ico'), 83 | }, 84 | }, 85 | { 86 | name: '@electron-forge/maker-zip', 87 | platforms: ['darwin'], 88 | }, 89 | { 90 | name: '@electron-forge/maker-deb', 91 | config: { 92 | options: { 93 | homepage: awsaml.repository.url.replace('.git', ''), 94 | maintainer: awsaml.contributors.join(', '), 95 | icon: 'images/icon.png', 96 | }, 97 | }, 98 | }, 99 | ], 100 | publishers: [ 101 | { 102 | name: '@electron-forge/publisher-github', 103 | config: { 104 | repository: { 105 | owner: 'rapid7', 106 | name: 'awsaml', 107 | }, 108 | draft: true, 109 | prerelease: false, 110 | }, 111 | }, 112 | ], 113 | }; 114 | 115 | // If we're running in Jenkins (or the env indicates we are) attempt to 116 | // code sign. 117 | if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== '') { 118 | config.packagerConfig.osxSign = {}; 119 | config.packagerConfig.osxNotarize = { 120 | tool: 'notarytool', 121 | appleId: process.env.NOTARIZE_CREDS_USR, 122 | appleIdPassword: process.env.NOTARIZE_CREDS_PSW, 123 | teamId: process.env.MAC_TEAM_ID, 124 | }; 125 | } 126 | 127 | module.exports = config; 128 | -------------------------------------------------------------------------------- /src/main/menu.js: -------------------------------------------------------------------------------- 1 | const { 2 | Menu, 3 | app, 4 | autoUpdater, 5 | } = require('electron'); 6 | const packageJson = require('../../package.json'); 7 | 8 | const d = new Date(); 9 | const isMac = process.platform === 'darwin'; 10 | const name = 'Awsaml'; 11 | const padAndreplaceEmail = (el) => `${' '.repeat(25)}${el.replace(/<([^;]*)>/, '').trim()}`; 12 | const contributors = [ 13 | ...packageJson.contributors.map(padAndreplaceEmail), 14 | '\nSpecial thanks to:\n', 15 | ...packageJson.thanks.map(padAndreplaceEmail), 16 | '\n', 17 | ].join('\n'); 18 | 19 | app.setAboutPanelOptions({ 20 | applicationName: name, 21 | applicationVersion: packageJson.version, 22 | copyright: `Copyright (c) Rapid7 ${d.getFullYear()} (${packageJson.license})`, 23 | [isMac ? 'credits' : 'authors']: contributors, 24 | }); 25 | 26 | const template = [ 27 | ...(isMac ? [{ 28 | label: name, 29 | submenu: [{ 30 | label: `About ${name}`, 31 | role: 'about', 32 | }, { 33 | label: 'Check For Updates...', 34 | click: () => { 35 | if (app.isPackaged) { 36 | autoUpdater.checkForUpdates(); 37 | } 38 | }, 39 | }, { 40 | type: 'separator', 41 | }, { 42 | label: 'Services', 43 | role: 'services', 44 | submenu: [], 45 | }, { 46 | type: 'separator', 47 | }, { 48 | accelerator: 'Command+H', 49 | label: `Hide ${name}`, 50 | role: 'hide', 51 | }, { 52 | accelerator: 'Command+Shift+H', 53 | label: 'Hide Others', 54 | role: 'hideothers', 55 | }, { 56 | label: 'Show All', 57 | role: 'unhide', 58 | }, { 59 | type: 'separator', 60 | }, { 61 | accelerator: 'Command+Q', 62 | click() { 63 | app.quit(); 64 | }, 65 | label: 'Quit', 66 | }], 67 | }] : []), 68 | { 69 | label: 'Edit', 70 | submenu: [{ 71 | accelerator: 'CmdOrCtrl+Z', 72 | label: 'Undo', 73 | role: 'undo', 74 | }, { 75 | accelerator: 'Shift+CmdOrCtrl+Z', 76 | label: 'Redo', 77 | role: 'redo', 78 | }, { 79 | type: 'separator', 80 | }, { 81 | accelerator: 'CmdOrCtrl+X', 82 | label: 'Cut', 83 | role: 'cut', 84 | }, { 85 | accelerator: 'CmdOrCtrl+C', 86 | label: 'Copy', 87 | role: 'copy', 88 | }, { 89 | accelerator: 'CmdOrCtrl+V', 90 | label: 'Paste', 91 | role: 'paste', 92 | }, { 93 | accelerator: 'CmdOrCtrl+A', 94 | label: 'Select All', 95 | role: 'selectall', 96 | }], 97 | }, { 98 | label: 'View', 99 | submenu: [{ 100 | accelerator: 'CmdOrCtrl+R', 101 | click(item, focusedWindow) { 102 | if (focusedWindow) { 103 | focusedWindow.reload(); 104 | } 105 | }, 106 | label: 'Reload', 107 | }, { 108 | accelerator: 'CmdOrCtrl+Shift+R', 109 | click(item, focusedWindow) { 110 | if (focusedWindow) { 111 | focusedWindow.emit('reset'); 112 | } 113 | }, 114 | label: 'Reset', 115 | }, { 116 | accelerator: (function a() { 117 | return (process.platform === 'darwin') ? 'Ctrl+Command+F' : 'F11'; 118 | }()), 119 | click(item, focusedWindow) { 120 | if (focusedWindow) { 121 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); 122 | } 123 | }, 124 | label: 'Toggle Full Screen', 125 | }, 126 | { 127 | accelerator: (function a() { 128 | return (process.platform === 'darwin') ? 'Alt+Command+I' : 'Ctrl+Shift+I'; 129 | }()), 130 | click(item, focusedWindow) { 131 | if (focusedWindow) { 132 | focusedWindow.toggleDevTools(); 133 | } 134 | }, 135 | label: 'Toggle Developer Tools', 136 | }], 137 | }, { 138 | label: 'Window', 139 | role: 'window', 140 | submenu: [{ 141 | accelerator: 'CmdOrCtrl+M', 142 | label: 'Minimize', 143 | role: 'minimize', 144 | }, { 145 | accelerator: 'CmdOrCtrl+W', 146 | label: 'Close', 147 | role: 'close', 148 | }, 149 | ...(isMac ? [{ 150 | type: 'separator', 151 | }, { 152 | label: 'Bring All to Front', 153 | role: 'front', 154 | }] : [])], 155 | }, 156 | ]; 157 | 158 | const menu = Menu.buildFromTemplate(template); 159 | 160 | Menu.setApplicationMenu(menu); 161 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awsaml", 3 | "version": "4.0.0", 4 | "description": "Periodically refreshes AWS access keys", 5 | "license": "MIT", 6 | "contributors": [ 7 | "Opal Mitchell", 8 | "Dave Greene", 9 | "Marguerite Martinez", 10 | "Andrea Nguyen" 11 | ], 12 | "thanks": [ 13 | "Tristan Harward" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/rapid7/awsaml.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/rapid7/awsaml/issues" 21 | }, 22 | "engines": { 23 | "node": ">=16.0.0" 24 | }, 25 | "packageManager": "yarn@4.9.2", 26 | "scripts": { 27 | "electron": "electron src/main/index.js", 28 | "electron-dev": "NODE_ENV=development ELECTRON_START_URL=http://localhost:3000 electron src/main/index.js", 29 | "react-start": "BROWSER=none; NODE_ENV=development craco start", 30 | "react-build": "craco build", 31 | "test": "jest", 32 | "lint": "eslint '*.js' 'src/**/*.js' 'test/**/*.js'", 33 | "report": "coveralls < ./coverage/lcov.info", 34 | "build": "node build.js", 35 | "show-appcast-checkpoint": "curl --compressed --location --user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36' 'https://github.com/rapid7/awsaml/releases.atom' | /usr/bin/sed 's|[^<]*||g' | shasum --algorithm 256", 36 | "start": "electron-forge start", 37 | "package": "electron-forge package", 38 | "make": "electron-forge make", 39 | "publish": "electron-forge publish", 40 | "clean": "rm -rf out && rm -rf build" 41 | }, 42 | "homepage": "./", 43 | "proxy": "http://localhost:2600/", 44 | "main": "src/main/index.js", 45 | "dependencies": { 46 | "@aws-sdk/client-sts": "^3.614.0", 47 | "@node-saml/passport-saml": "^5.0.0", 48 | "@xmldom/xmldom": "^0.8.10", 49 | "body-parser": "^1.20.2", 50 | "electron-log": "^5.1.5", 51 | "electron-squirrel-startup": "^1.0.1", 52 | "express": "^4.19.2", 53 | "express-session": "^1.18.0", 54 | "ini": "^4.1.3", 55 | "morgan": "^1.10.0", 56 | "passport": "^0.7.0", 57 | "react": "^18.3.1", 58 | "react-dom": "^18.3.1", 59 | "stylis": "^4.3.2", 60 | "update-electron-app": "^3.0.0", 61 | "uuid": "^10.0.0", 62 | "xpath.js": "^1.1.0" 63 | }, 64 | "devDependencies": { 65 | "@babel/core": "^7.24.9", 66 | "@babel/eslint-parser": "^7.24.8", 67 | "@babel/preset-env": "^7.24.8", 68 | "@babel/preset-react": "^7.24.7", 69 | "@craco/craco": "^7.1.0", 70 | "@electron-forge/cli": "^7.4.0", 71 | "@electron-forge/core": "^7.4.0", 72 | "@electron-forge/maker-deb": "^7.4.0", 73 | "@electron-forge/maker-rpm": "^7.4.0", 74 | "@electron-forge/maker-squirrel": "^7.4.0", 75 | "@electron-forge/maker-zip": "^7.4.0", 76 | "@electron-forge/plugin-webpack": "^7.4.0", 77 | "@electron-forge/publisher-github": "^7.4.0", 78 | "@electron/get": "^3.0.0", 79 | "@electron/rebuild": "^3.6.0", 80 | "@fortawesome/fontawesome-free": "^6.5.2", 81 | "@fortawesome/fontawesome-svg-core": "^6.5.2", 82 | "@fortawesome/free-brands-svg-icons": "^6.5.2", 83 | "@fortawesome/free-regular-svg-icons": "^6.5.2", 84 | "@fortawesome/free-solid-svg-icons": "^6.5.2", 85 | "@fortawesome/react-fontawesome": "^0.2.2", 86 | "@popperjs/core": "^2.11.8", 87 | "babel-jest": "^29.7.0", 88 | "bootstrap": "^5.3.3", 89 | "coveralls": "^3.1.1", 90 | "electron": "^31.2.0", 91 | "electron-packager": "^17.1.2", 92 | "eslint": "^8.57.0", 93 | "eslint-config-airbnb": "^19.0.4", 94 | "eslint-plugin-import": "^2.29.1", 95 | "eslint-plugin-jest": "^28.6.0", 96 | "eslint-plugin-jsx-a11y": "^6.9.0", 97 | "eslint-plugin-node": "^11.1.0", 98 | "eslint-plugin-react": "^7.34.4", 99 | "eslint-plugin-react-hooks": "^4.6.2", 100 | "glob": "^11.0.0", 101 | "history": "^5.3.0", 102 | "immutability-helper": "^3.1.1", 103 | "jest": "^29.7.0", 104 | "jest-junit": "^16.0.0", 105 | "prismjs": "^1.29.0", 106 | "prop-types": "^15.8.1", 107 | "react-dnd": "^16.0.1", 108 | "react-dnd-html5-backend": "^16.0.1", 109 | "react-is": "^18.3.1", 110 | "react-router": "^6.24.1", 111 | "react-router-dom": "^6.24.1", 112 | "react-scripts": "^5.0.1", 113 | "reactstrap": "^9.2.2", 114 | "should": "^13.2.3", 115 | "styled-components": "^6.1.11", 116 | "typescript": "^4.9.5" 117 | }, 118 | "browserslist": { 119 | "production": [ 120 | "last 1 electron version" 121 | ], 122 | "development": [ 123 | "last 1 electron version" 124 | ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | const { 3 | app, 4 | BrowserWindow, 5 | ipcMain, 6 | nativeTheme, 7 | clipboard, 8 | } = require('electron'); 9 | const log = require('electron-log'); 10 | const { app: Server } = require('./api/server'); 11 | const { loadTouchBar } = require('./touchbar'); 12 | const protocol = require('./protocol'); 13 | const { channels } = require('./containers/index'); 14 | 15 | log.initialize({ preload: true }); 16 | 17 | // See https://www.electronforge.io/config/makers/squirrel.windows#handling-startup-events 18 | // for more details. 19 | if (require('electron-squirrel-startup')) { 20 | app.quit(); 21 | } 22 | 23 | // Bootstrap the updater 24 | if (app.isPackaged) { 25 | const { updateElectronApp } = require('update-electron-app'); 26 | updateElectronApp(); 27 | } 28 | 29 | const isPlainObject = (value) => Object.prototype.toString.call(value) === '[object Object]'; 30 | const storagePath = path.join(app.getPath('userData'), 'data.json'); 31 | const isDev = process.env.NODE_ENV === 'development'; 32 | const WindowWidth = 800; 33 | const WindowHeight = 800; 34 | 35 | let mainWindow = null; 36 | let baseUrl = process.env.ELECTRON_START_URL || Server.get('baseUrl'); 37 | 38 | global.Storage = require('./api/storage')(storagePath); 39 | global.Manager = require('./api/reloader/manager')(); 40 | 41 | let storedMetadataUrls = Storage.get('metadataUrls') || []; 42 | 43 | // Migrate from old metadata url storage schema to new one 44 | if (isPlainObject(storedMetadataUrls)) { 45 | storedMetadataUrls = Object.keys(storedMetadataUrls).map((k) => ({ 46 | name: storedMetadataUrls[k], 47 | url: k, 48 | })); 49 | 50 | Storage.set('metadataUrls', storedMetadataUrls); 51 | } 52 | 53 | // Disable cache 54 | app.commandLine.appendSwitch('disable-http-cache'); 55 | // No reason for Awsaml to force Macs to use dedicated gfx 56 | app.disableHardwareAcceleration(); 57 | 58 | app.on('window-all-closed', () => { 59 | app.quit(); 60 | }); 61 | 62 | let lastWindowState = Storage.get('lastWindowState'); 63 | 64 | if (lastWindowState === null) { 65 | lastWindowState = { 66 | height: WindowHeight, 67 | width: WindowWidth, 68 | }; 69 | } 70 | 71 | protocol.registerSchemas(); 72 | 73 | app.on('ready', async () => { 74 | // eslint-disable-next-line global-require 75 | require('./menu'); 76 | 77 | protocol.registerHandlers(); 78 | 79 | const host = Server.get('host'); 80 | const port = Server.get('port'); 81 | 82 | Server.listen(port, host, () => { 83 | log.info(`Server listening on ${host}:${port}`); 84 | }); 85 | 86 | Storage.set('session', {}); 87 | 88 | mainWindow = new BrowserWindow({ 89 | height: lastWindowState.height, 90 | show: false, 91 | title: 'Rapid7 - Awsaml', 92 | icon: 'images/icon.png', 93 | webPreferences: { 94 | preload: path.join(__dirname, 'preload.js'), 95 | }, 96 | width: lastWindowState.width, 97 | x: lastWindowState.x, 98 | y: lastWindowState.y, 99 | }); 100 | 101 | mainWindow.on('close', () => { 102 | log.info('[Event] BrowserWindow close'); 103 | const bounds = mainWindow.getBounds(); 104 | 105 | Storage.set('lastWindowState', { 106 | height: bounds.height, 107 | version: 1, 108 | width: bounds.width, 109 | x: bounds.x, 110 | y: bounds.y, 111 | }); 112 | 113 | Storage.delete('session'); 114 | Storage.delete('authenticated'); 115 | Storage.delete('multipleRoles'); 116 | }); 117 | 118 | mainWindow.on('closed', () => { 119 | log.info('[Event] BrowserWindow closed'); 120 | mainWindow = null; 121 | }); 122 | 123 | if (isDev) { 124 | mainWindow.openDevTools({ mode: 'detach' }); 125 | } else { 126 | baseUrl = new URL(`awsaml:///${path.join(__dirname, '/../../build/index.html')}`).toString(); 127 | log.info(`baseUrl: ${baseUrl}`); 128 | Server.set('baseUrl', baseUrl); 129 | } 130 | 131 | mainWindow.on('ready-to-show', () => { 132 | log.info('[Event] BrowserWindow ready-to-show'); 133 | mainWindow.show(); 134 | }); 135 | 136 | log.info('BrowserWindow.loadURL'); 137 | await mainWindow.loadURL(baseUrl); 138 | log.info('BrowserWindow.loadURL completed'); 139 | 140 | mainWindow.webContents.on('did-finish-load', () => { 141 | log.info('[Event] BrowserWindow did-finish-load'); 142 | loadTouchBar(mainWindow, storedMetadataUrls); 143 | }); 144 | 145 | // set up IPC handlers 146 | Object.entries(channels).forEach(([namespace, value = {}]) => { 147 | log.info(`Loading handlers for ${namespace}`); 148 | Object.entries(value).forEach(([channelName, handler]) => { 149 | ipcMain.handle(channelName, handler); 150 | }); 151 | }); 152 | 153 | // set up dark mode handler 154 | ipcMain.handle('dark-mode:get', () => nativeTheme.shouldUseDarkColors); 155 | nativeTheme.on('updated', () => { 156 | mainWindow.webContents.send('dark-mode:updated', nativeTheme.shouldUseDarkColors); 157 | }); 158 | 159 | // set up clipboard handler 160 | ipcMain.handle('copy', async (event, value) => { 161 | clipboard.writeText(value); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | 5 | ## [2.3.0] - 2022-03-23 6 | ### Fixed 7 | - Fix failures to login when moving between commercial/fedramp accounts: [#192](https://github.com/rapid7/awsaml/pull/192) 8 | - Fix tests and enable github actions: [#193](https://github.com/rapid7/awsaml/pull/193) 9 | - Add keyboard shortcut for Reset: [#180](https://github.com/rapid7/awsaml/pull/180) 10 | - Bump packages called out by dependabot: [#196](https://github.com/rapid7/awsaml/pull/196) 11 | - Bump electron packager to latest version: [197](https://github.com/rapid7/awsaml/pull/197) 12 | - Bump other packages to latest versions: [#194](https://github.com/rapid7/awsaml/pull/194) 13 | 14 | ## [2.2.3] - 2022-02-09 15 | ### Fixed 16 | - Adds ability to work with AWSGovCloud [#181](https://github.com/rapid7/awsaml/pull/181) 17 | 18 | ## [2.2.2] - 2019-09-12 19 | ### Fixed 20 | - Upgraded electron bits to version that supports WebAuthn standard [#135](https://github.com/rapid7/awsaml/issues/135) 21 | 22 | ## [2.2.1] - 2019-08-08 23 | ### Fixed 24 | - Upgraded electron and everything else, fixing [#126](https://github.com/rapid7/awsaml/issues/126) 25 | 26 | ## [2.2.0] - 2019-08-07 27 | ### Added 28 | - Adds ability to proactively name new profiles ([#125](https://github.com/rapid7/awsaml/pull/125)) 29 | 30 | ### Changed 31 | - Disables hardware acceleration, giving you more battery time ([#123](https://github.com/rapid7/awsaml/pull/123)) 32 | - Documentation updates ([#124](https://github.com/rapid7/awsaml/pull/124)) 33 | 34 | ## [2.1.0] - 2018-08-08 35 | ### Added 36 | - Search for recent logins ([#120](https://github.com/rapid7/awsaml/pull/120)) 37 | - Multiple role support ([#119](https://github.com/rapid7/awsaml/pull/119)) 38 | - Copy/paste button for terminal export commands 39 | 40 | ### Changed 41 | - Backend improvements to identify each recent login profile with a unique UUID 42 | 43 | ## [2.0.0] - 2018-07-26 44 | This release represents a massive overhaul to how Awsaml's internals work. There are no changes to the way users 45 | interact with the tool aside from some nice bells and whistles. However, internally some of the changes include: 46 | 47 | ### Changed 48 | - Created new SPA frontend app and deprecated express-react-views 49 | - Refactored the backend API to handle new SPA-based workflow 50 | - Frontend refresh; updated Bootstrap and added Font-Awesome for icons 51 | - Simplified and improved build process 52 | - Added "copy to clipboard" buttons where appropriate 53 | 54 | ## [1.6.1] - 2018-06-13 55 | ### Fixed 56 | - Remove unused dependencies 57 | 58 | ## [1.6.0] - 2018-06-13 59 | ### Added 60 | - Add profile login buttons to touchbar 61 | - Add touchbar buttons for refresh/logout 62 | 63 | ### Changed 64 | - Refresh credentials every half-duration 65 | - Rebuild only prod dependencies. 66 | - Refactor build script into stand-alone js file so we can use electron-packager hooks to force a dependency rebuild. 67 | - Bump aws-sdk and electron packages. 68 | - Load prism.js from local file for language compatibility. 69 | - Remove unneeded css. 70 | - Update jsx to work correctly with updated external dependencies. 71 | - Update external css and js dependencies. 72 | - Disable react/jsx-filename-extension as express-react-views requires views with .jsx extensions. 73 | - Use symlinks instead of copying files to make macOS zips smaller. 74 | - Update/pin dependencies to pick up security fixes. 75 | - Update docs to denote use of Yarn and Node v7. 76 | 77 | ### Fixed 78 | - Fix code linting errors. 79 | - Fix issues around PropTypes being split from React core. 80 | 81 | ## [1.5.0] - 2017-08-25 82 | ### Added 83 | - Ability to use custom names for "Recent Logins" profiles by @udangel-r7. 84 | - Login button for "Recent Logins" profiles by @udangel-r7. 85 | - yarn.lock file to pin package version changes by @davepgreene. 86 | 87 | ### Changed 88 | - Electron to pin version at 1.6.11 by @onefrankguy. 89 | - Build process to allow platform to be set at run time by @onefrankguy. 90 | - Build process to use "electron" instead of "electron-prebuilt" by @erran. 91 | 92 | ## [1.4.0] - 2016-11-21 93 | ### Added 94 | - :construction_worker: Enable TravisCI builds for continuous integration by @erran! 95 | - Support a list of "recent logins" on the configure page by @erran. 96 | 97 | ## [1.3.0] - 2016-09-28 98 | ### Added 99 | - Homebrew cask support by @fpedrini. 100 | - Tests for the AwsCredentials class by @onefrankguy. 101 | - Transpiler tooling to make frontend/backend splitting easier by @dgreene-r7. 102 | - .nvmrc file to pin to the latest LTS release by @davepgreene. 103 | 104 | ### Changed 105 | - Credentials so they default to hidden in the UI by @erran. 106 | - Electron packaging so tests are excluded from releases by @onefrankguy. 107 | - Routes and server config to reside in their own source files by @davepgreene. 108 | 109 | ### Fixed 110 | - Issue where empty storage files caused uncaught exceptions by @erran. 111 | - Issue where automatic token renewal failed after logout by @onefrankguy. 112 | 113 | ## [1.2.0] - 2016-05-13 114 | ### Added 115 | - Ability to run server backend locally without Electron by @dgreene-r7. 116 | - Logout button and server endpoint by @devkmsg. 117 | - Mocha test framework by @onefrankguy. 118 | 119 | ### Changed 120 | - Electron version to 0.37.8 by @onefrankguy. 121 | 122 | ### Fixed 123 | - Documentation to use the correct audience restriction URL by @erran. 124 | 125 | ## [1.1.0] - 2016-02-22 126 | ### Added 127 | - Display of error messages for invalid metadata URLs by @dgreene-r7. 128 | - Display of setup commands to make configuring CLI tools easier by @dgreene-r7. 129 | - Display of AWS account ID to make using multiple accounts easier by @athompson-r7. 130 | 131 | ### Changed 132 | - Electron version to 0.36.7 by @onefrankguy. 133 | - Code formatting to match the Rapid7 Style Guide by @dgreene-r7. 134 | 135 | ### Fixed 136 | - Issue where refresh button didn't work after session timeout by @dgreene-r7. 137 | 138 | ## [1.0.0] - 2016-01-19 139 | ### Added 140 | - Initial release by @onefrankguy. 141 | 142 | [Unreleased]: https://github.com/rapid7/awsaml/compare/v2.1.0...HEAD 143 | [2.1.0]: https://github.com/rapid7/awsaml/compare/v2.0.0...v2.1.0 144 | [2.0.0]: https://github.com/rapid7/awsaml/compare/v1.6.1...v2.0.0 145 | [1.6.1]: https://github.com/rapid7/awsaml/compare/v1.6.0...v1.6.1 146 | [1.6.0]: https://github.com/rapid7/awsaml/compare/v1.5.0...v1.6.0 147 | [1.5.0]: https://github.com/rapid7/awsaml/compare/v1.4.0...v1.5.0 148 | [1.4.0]: https://github.com/rapid7/awsaml/compare/v1.3.0...v1.4.0 149 | [1.3.0]: https://github.com/rapid7/awsaml/compare/v1.2.0...v1.3.0 150 | [1.2.0]: https://github.com/rapid7/awsaml/compare/v1.1.0...v1.2.0 151 | [1.1.0]: https://github.com/rapid7/awsaml/compare/v1.0.0...v1.1.0 152 | [1.0.0]: https://github.com/rapid7/awsaml/tree/v1.0.0 153 | -------------------------------------------------------------------------------- /src/renderer/containers/configure/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { 5 | InputGroup, 6 | Input, 7 | ListGroupItem, 8 | Button, 9 | Collapse, 10 | } from 'reactstrap'; 11 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 12 | import { useDrag, useDrop } from 'react-dnd'; 13 | import { 14 | BORDER_COLOR_SCHEME_MEDIA_QUERY, 15 | } from '../../constants/styles'; 16 | import InputGroupWithCopyButton from '../components/InputGroupWithCopyButton'; 17 | 18 | const ProfileInputGroup = styled(InputGroup)` 19 | width: 100%; 20 | height: 2.5em; 21 | line-height: 2.5em; 22 | `; 23 | 24 | const TransparentlistGroupItem = styled(ListGroupItem)` 25 | background-color: transparent; 26 | 27 | ${BORDER_COLOR_SCHEME_MEDIA_QUERY} 28 | `; 29 | 30 | const PaddedCollapse = styled(Collapse)` 31 | margin-top: 0.4rem; 32 | `; 33 | 34 | const LoginType = 'login'; 35 | 36 | function Login(props) { 37 | const { 38 | url, 39 | pretty, 40 | profileUuid, 41 | deleteCallback, 42 | errorHandler, 43 | darkMode, 44 | index, 45 | moveLogin, 46 | } = props; 47 | 48 | const [profileName, setProfileName] = useState(''); 49 | const [isOpen, setIsOpen] = useState(false); 50 | const [caretDirection, setCaretDirection] = useState('right'); 51 | 52 | const ref = useRef(null); 53 | const [{ handlerId }, drop] = useDrop({ 54 | accept: LoginType, 55 | collect(monitor) { 56 | return { 57 | handlerId: monitor.getHandlerId(), 58 | }; 59 | }, 60 | hover(item, monitor) { 61 | if (!ref.current) { 62 | return; 63 | } 64 | const dragIndex = item.index; 65 | const hoverIndex = index; 66 | // Don't replace items with themselves 67 | if (dragIndex === hoverIndex) { 68 | return; 69 | } 70 | // Determine rectangle on screen 71 | const hoverBoundingRect = ref.current?.getBoundingClientRect(); 72 | // Get vertical middle 73 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; 74 | // Determine mouse position 75 | const clientOffset = monitor.getClientOffset(); 76 | // Get pixels to the top 77 | const hoverClientY = clientOffset.y - hoverBoundingRect.top; 78 | // Only perform the move when the mouse has crossed half of the items height 79 | // When dragging downwards, only move when the cursor is below 50% 80 | // When dragging upwards, only move when the cursor is above 50% 81 | 82 | // Dragging downwards 83 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 84 | return; 85 | } 86 | // Dragging upwards 87 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 88 | return; 89 | } 90 | // Time to actually perform the action 91 | moveLogin(dragIndex, hoverIndex); 92 | // Note: we're mutating the monitor item here! 93 | // Generally it's better to avoid mutations, 94 | // but it's good here for the sake of performance 95 | // to avoid expensive index searches. 96 | // eslint-disable-next-line no-param-reassign 97 | item.index = hoverIndex; 98 | }, 99 | }); 100 | const [{ isDragging }, drag] = useDrag(() => ({ 101 | type: LoginType, 102 | collect: (monitor) => ({ 103 | isDragging: monitor.isDragging(), 104 | }), 105 | })); 106 | 107 | const handleInputChange = ({ target: { value } }) => { 108 | setProfileName(value); 109 | }; 110 | 111 | const handleKeyDown = (event) => { 112 | if (event.keyCode !== 32) { // Spacebar 113 | return; 114 | } 115 | event.preventDefault(); 116 | // eslint-disable-next-line no-param-reassign 117 | event.currentTarget.value += ' '; 118 | }; 119 | 120 | const handleSubmit = async (event) => { 121 | event.preventDefault(); 122 | 123 | const payload = { 124 | metadataUrl: url, 125 | profileName: profileName || pretty, 126 | profileUuid, 127 | }; 128 | 129 | const { 130 | error, 131 | redirect, 132 | } = await window.electronAPI.login(payload); 133 | 134 | if (error) { 135 | errorHandler(error); 136 | } 137 | if (redirect) { 138 | document.location.replace(redirect); 139 | } 140 | }; 141 | 142 | const handleDelete = async (event) => { 143 | event.preventDefault(); 144 | 145 | const text = `Are you sure you want to delete the profile "${profileName || pretty}"?`; 146 | 147 | // eslint-disable-next-line no-alert 148 | if (window.confirm(text)) { 149 | const payload = { 150 | params: { profileUuid }, 151 | }; 152 | 153 | await window.electronAPI.deleteProfile({ profileUuid }); 154 | deleteCallback(payload); 155 | } 156 | }; 157 | 158 | const handleCollapse = () => { 159 | setCaretDirection(caretDirection === 'right' ? 'down' : 'right'); 160 | setIsOpen(!isOpen); 161 | }; 162 | 163 | const opacity = isDragging ? 0 : 1; 164 | drag(drop(ref)); 165 | 166 | return ( 167 |
174 | 175 | 176 | 182 | 190 | 197 | 204 | 205 | 206 | 212 | 213 | 214 |
215 | ); 216 | } 217 | 218 | Login.propTypes = { 219 | pretty: PropTypes.string, 220 | profileUuid: PropTypes.string.isRequired, 221 | url: PropTypes.string.isRequired, 222 | deleteCallback: PropTypes.func.isRequired, 223 | errorHandler: PropTypes.func.isRequired, 224 | darkMode: PropTypes.bool.isRequired, 225 | index: PropTypes.number.isRequired, 226 | moveLogin: PropTypes.func.isRequired, 227 | }; 228 | 229 | Login.defaultProps = { 230 | pretty: '', 231 | }; 232 | 233 | export default Login; 234 | -------------------------------------------------------------------------------- /src/main/containers/configure.js: -------------------------------------------------------------------------------- 1 | const https = require('node:https'); 2 | const { v4: uuidv4 } = require('uuid'); 3 | const { DOMParser } = require('@xmldom/xmldom'); 4 | const xpath = require('xpath.js'); 5 | 6 | const { 7 | app, 8 | auth, 9 | } = require('../api/server'); 10 | const ResponseObj = require('../api/response'); 11 | const config = require('../api/config.json'); 12 | 13 | const Errors = { 14 | invalidMetadataErr: 'The SAML metadata is invalid.', 15 | urlInvalidErr: 'The SAML metadata URL is invalid.', 16 | uuidInvalidError: 'The profile is invalid.', 17 | }; 18 | 19 | async function getMetadataUrls() { 20 | let migrated = false; 21 | const storedMetadataUrls = (Storage.get('metadataUrls') || []).map((metadata) => { 22 | const ret = { 23 | ...metadata, 24 | }; 25 | if (metadata.profileUuid === undefined) { 26 | migrated = true; 27 | ret.profileUuid = uuidv4(); 28 | } 29 | 30 | return metadata; 31 | }); 32 | 33 | if (migrated) { 34 | Storage.set('metadataUrls', storedMetadataUrls); 35 | } 36 | 37 | return storedMetadataUrls; 38 | } 39 | 40 | async function setMetadataUrls(event, metadataUrls) { 41 | Storage.set('metadataUrls', metadataUrls); 42 | } 43 | 44 | async function getDefaultMetadata() { 45 | const storedMetadataUrls = (Storage.get('metadataUrls') || []); 46 | 47 | let defaultMetadataName = app.get('profileName') || ''; 48 | 49 | // We populate the value of the metadata url field on the following (in order of precedence): 50 | // 1. Use the current session's metadata url (may have been rejected). 51 | // 2. Use the latest validated metadata url. 52 | // 3. Support the <= v1.3.0 storage key. 53 | // 4. Default the metadata url to empty string. 54 | let defaultMetadataUrl = app.get('metadataUrl') 55 | || Storage.get('previousMetadataUrl') 56 | || Storage.get('metadataUrl') 57 | || ''; 58 | 59 | if (!defaultMetadataUrl) { 60 | if (storedMetadataUrls.length > 0) { 61 | const defaultMetadata = storedMetadataUrls[0]; 62 | if (Object.prototype.hasOwnProperty.call(defaultMetadata, 'url')) { 63 | defaultMetadataUrl = defaultMetadata.url; 64 | } 65 | if (Object.prototype.hasOwnProperty.call(defaultMetadata, 'name')) { 66 | defaultMetadataName = defaultMetadata.name; 67 | } 68 | } 69 | } 70 | 71 | return { 72 | url: defaultMetadataUrl, 73 | name: defaultMetadataName, 74 | }; 75 | } 76 | 77 | async function asyncHttpsGet(url) { 78 | return new Promise((resolve, reject) => { 79 | let data = ''; 80 | 81 | // eslint-disable-next-line consistent-return 82 | https.get(url, (res) => { 83 | if (res.statusCode !== 200) { 84 | return reject(res); 85 | } 86 | 87 | res.on('data', (chunk) => { 88 | data += chunk; 89 | }); 90 | 91 | res.on('end', () => { 92 | resolve(data); 93 | }); 94 | }); 95 | }); 96 | } 97 | 98 | async function deleteProfile(event, payload) { 99 | const { 100 | profileUuid, 101 | } = payload; 102 | 103 | let metadataUrls = Storage.get('metadataUrls'); 104 | 105 | metadataUrls = metadataUrls 106 | .map((metadata) => ((metadata.profileUuid !== profileUuid) ? metadata : null)) 107 | .filter((el) => !!el); 108 | Storage.set('metadataUrls', metadataUrls); 109 | 110 | return {}; 111 | } 112 | 113 | async function getProfile(event, payload) { 114 | const { 115 | profileUuid, 116 | } = payload; 117 | 118 | const metadataUrls = Storage.get('metadataUrls'); 119 | const profile = metadataUrls.find((el) => el.profileUuid === profileUuid); 120 | 121 | return { 122 | profile, 123 | }; 124 | } 125 | 126 | async function login(event, payload) { 127 | const { 128 | profileUuid, 129 | profileName, 130 | metadataUrl, 131 | } = payload; 132 | 133 | let storedMetadataUrls = Storage.get('metadataUrls') || []; 134 | let profile; 135 | 136 | if (!metadataUrl) { 137 | Storage.set('metadataUrlValid', false); 138 | Storage.set('metadataUrlError', Errors.urlInvalidErr); 139 | 140 | return { 141 | ...ResponseObj, 142 | error: Errors.urlInvalidErr, 143 | metadataUrlValid: false, 144 | }; 145 | } 146 | 147 | // If a profileUuid is passed, validate it and update storage 148 | // with the submitted profile name. 149 | if (profileUuid) { 150 | profile = storedMetadataUrls.find((metadata) => metadata.profileUuid === profileUuid); 151 | 152 | if (!profile) { 153 | return { 154 | ...ResponseObj, 155 | error: Errors.uuidInvalidError, 156 | uuidUrlValid: false, 157 | }; 158 | } 159 | 160 | if (profile.url !== metadataUrl) { 161 | return { 162 | ...ResponseObj, 163 | error: Errors.urlInvalidErr, 164 | metadataUrlValid: false, 165 | }; 166 | } 167 | 168 | if (profileName) { 169 | storedMetadataUrls = storedMetadataUrls.map((metadata) => { 170 | const ret = { 171 | ...metadata, 172 | }; 173 | 174 | if (metadata.profileUuid === profileUuid && metadata.name !== profileName) { 175 | ret.name = profileName; 176 | } 177 | 178 | return ret; 179 | }); 180 | Storage.set('metadataUrls', storedMetadataUrls); 181 | } 182 | } else { 183 | profile = storedMetadataUrls.find((metadata) => metadata.url === metadataUrl); 184 | } 185 | 186 | app.set('metadataUrl', metadataUrl); 187 | app.set('profileName', profileName); 188 | 189 | const metaDataResponseObj = { 190 | ...ResponseObj, 191 | defaultMetadataName: profileName, 192 | defaultMetadataUrl: metadataUrl, 193 | }; 194 | 195 | let data; 196 | try { 197 | data = await asyncHttpsGet(metadataUrl); 198 | } catch (e) { 199 | Storage.set('metadataUrlValid', false); 200 | Storage.set('metadataUrlError', Errors.urlInvalidErr); 201 | 202 | return { 203 | ...metaDataResponseObj, 204 | error: Errors.urlInvalidErr, 205 | metadataUrlValid: false, 206 | }; 207 | } 208 | 209 | Storage.set('metadataUrlValid', true); 210 | Storage.set('metadataUrlError', null); 211 | 212 | const xmlDoc = new DOMParser().parseFromString(data, 'text/xml'); 213 | const safeXpath = (doc, p) => { 214 | try { 215 | return xpath(doc, p); 216 | } catch (_) { 217 | return null; 218 | } 219 | }; 220 | 221 | let cert = safeXpath(xmlDoc, '//*[local-name(.)=\'X509Certificate\']/text()'); 222 | let issuer = safeXpath(xmlDoc, '//*[local-name(.)=\'EntityDescriptor\']/@entityID'); 223 | let entryPoint = safeXpath(xmlDoc, '//*[local-name(.)=\'SingleSignOnService\' and' 224 | + ' @Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\']/@Location'); 225 | 226 | if (cert) { 227 | cert = cert.length ? cert[0].data.replace(/\s+/g, '') : null; 228 | } 229 | config.auth.idpCert = cert; 230 | 231 | if (issuer) { 232 | issuer = issuer.length ? issuer[0].value : null; 233 | } 234 | config.auth.issuer = issuer; 235 | 236 | if (entryPoint) { 237 | entryPoint = entryPoint.length ? entryPoint[0].value : null; 238 | } 239 | config.auth.entryPoint = entryPoint; 240 | app.set('lastEntryPointLoad', new Date()); 241 | 242 | if (!cert || !issuer || !entryPoint) { 243 | return { 244 | ...metaDataResponseObj, 245 | error: Errors.invalidMetadataErr, 246 | }; 247 | } 248 | 249 | Storage.set('previousMetadataUrl', metadataUrl); 250 | 251 | // Add a profile for this URL if one does not already exist 252 | if (!profile) { 253 | const metadataUrls = Storage.get('metadataUrls') || []; 254 | 255 | Storage.set( 256 | 'metadataUrls', 257 | metadataUrls.concat([ 258 | { 259 | name: profileName || metadataUrl, 260 | profileUuid: uuidv4(), 261 | url: metadataUrl, 262 | }, 263 | ]), 264 | ); 265 | } 266 | 267 | app.set('entryPointUrl', config.auth.entryPoint); 268 | auth.configure(config.auth); 269 | 270 | return { 271 | redirect: config.auth.entryPoint, 272 | }; 273 | } 274 | 275 | async function isAuthenticated() { 276 | return Storage.get('authenticated') || false; 277 | } 278 | 279 | async function hasMultipleRoles() { 280 | return Storage.get('multipleRoles') || false; 281 | } 282 | 283 | module.exports = { 284 | getMetadataUrls, 285 | setMetadataUrls, 286 | getDefaultMetadata, 287 | login, 288 | deleteProfile, 289 | getProfile, 290 | isAuthenticated, 291 | hasMultipleRoles, 292 | }; 293 | -------------------------------------------------------------------------------- /test/aws-credentials.js: -------------------------------------------------------------------------------- 1 | import Path from 'path'; 2 | import FS from 'fs'; 3 | import ini from 'ini'; 4 | import AwsCredentials from '../src/main/api/aws-credentials'; 5 | 6 | describe('AwsCredentials#saveAsIniFile', () => { 7 | const awsFolder = Path.resolve(__dirname, '.aws'); 8 | const awsCredentials = Path.resolve(awsFolder, 'credentials'); 9 | 10 | beforeEach(() => { 11 | if (FS.existsSync(awsCredentials)) { 12 | FS.unlinkSync(awsCredentials); 13 | } 14 | 15 | if (FS.existsSync(awsFolder)) { 16 | FS.rmdirSync(awsFolder); 17 | } 18 | }); 19 | 20 | it('returns an error when credentials are null', (done) => { 21 | const aws = new AwsCredentials(); 22 | 23 | aws.saveAsIniFile(null, 'profile', (error) => { 24 | expect(error.toString()).not.toEqual(''); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('returns an error when profile is null', (done) => { 30 | const aws = new AwsCredentials(); 31 | 32 | aws.saveAsIniFile({}, null, (error) => { 33 | expect(error.toString()).not.toEqual(''); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('returns an error when $HOME path is unresolved', (done) => { 39 | const aws = new AwsCredentials(); 40 | 41 | delete process.env.HOME; 42 | delete process.env.USERPROFILE; 43 | delete process.env.HOMEPATH; 44 | delete process.env.HOMEDRIVE; 45 | 46 | aws.saveAsIniFile({}, 'profile', (error) => { 47 | expect(error.toString()).not.toEqual(''); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('returns an error when $HOME path is empty', (done) => { 53 | const aws = new AwsCredentials(); 54 | 55 | process.env.HOME = ''; 56 | 57 | aws.saveAsIniFile({}, 'profile', (error) => { 58 | expect(error.toString()).not.toEqual(''); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('creates a $HOME/.aws folder when none exists', (done) => { 64 | const aws = new AwsCredentials(); 65 | 66 | process.env.HOME = __dirname; 67 | 68 | aws.saveAsIniFile({}, 'profile', (error) => { 69 | expect(FS.existsSync(awsFolder)).toBe(true); 70 | expect(error).toBeNull(); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('creates a $HOME/.aws folder with 0700 permissions', (done) => { 76 | const aws = new AwsCredentials(); 77 | 78 | process.env.HOME = __dirname; 79 | 80 | aws.saveAsIniFile({}, 'profile', (error) => { 81 | expect(FS.statSync(awsFolder).mode & 0x0700).toEqual(256); // eslint-disable-line no-bitwise 82 | expect(error).toBeNull(); 83 | done(); 84 | }); 85 | }); 86 | 87 | it('saves the access key in the credentials file', (done) => { 88 | const aws = new AwsCredentials(); 89 | const credentials = { 90 | AccessKeyId: 'AccessKeyId', 91 | }; 92 | 93 | process.env.HOME = __dirname; 94 | 95 | aws.saveAsIniFile(credentials, 'profile', (error) => { 96 | const data = FS.readFileSync(awsCredentials, 'utf-8'); 97 | const config = ini.parse(data); 98 | 99 | expect(error).toBeNull(); 100 | expect(config.profile.aws_access_key_id).toEqual(credentials.AccessKeyId); 101 | done(); 102 | }); 103 | }); 104 | 105 | it('saves the secret key in the credentials file', (done) => { 106 | const aws = new AwsCredentials(); 107 | const credentials = { 108 | SecretAccessKey: 'SecretAccessKey', 109 | }; 110 | 111 | process.env.HOME = __dirname; 112 | 113 | aws.saveAsIniFile(credentials, 'profile', (error) => { 114 | const data = FS.readFileSync(awsCredentials, 'utf-8'); 115 | const config = ini.parse(data); 116 | 117 | expect(error).toBeNull(); 118 | expect(config.profile.aws_secret_access_key).toEqual(credentials.SecretAccessKey); 119 | done(); 120 | }); 121 | }); 122 | 123 | it('saves the session token in the credentials file', (done) => { 124 | const aws = new AwsCredentials(); 125 | const credentials = { 126 | SessionToken: 'SessionToken', 127 | }; 128 | 129 | process.env.HOME = __dirname; 130 | 131 | aws.saveAsIniFile(credentials, 'profile', (error) => { 132 | const data = FS.readFileSync(awsCredentials, 'utf-8'); 133 | const config = ini.parse(data); 134 | 135 | expect(error).toBeNull(); 136 | expect(config.profile.aws_session_token).toEqual(credentials.SessionToken); 137 | done(); 138 | }); 139 | }); 140 | 141 | it( 142 | 'saves the session token as a security token in the credentials file', 143 | (done) => { 144 | const aws = new AwsCredentials(); 145 | const credentials = { 146 | SessionToken: 'SessionToken', 147 | }; 148 | 149 | process.env.HOME = __dirname; 150 | 151 | aws.saveAsIniFile(credentials, 'profile', (error) => { 152 | const data = FS.readFileSync(awsCredentials, 'utf-8'); 153 | const config = ini.parse(data); 154 | 155 | expect(error).toBeNull(); 156 | expect(config.profile.aws_security_token).toEqual(credentials.SessionToken); 157 | done(); 158 | }); 159 | }, 160 | ); 161 | 162 | it('saves the expiration in the credentials file if it exists', (done) => { 163 | const aws = new AwsCredentials(); 164 | const credentials = { 165 | AccessKeyId: 'AccessKeyId', 166 | SecretAccessKey: 'SecretAccessKey', 167 | SessionToken: 'SessionToken', 168 | Expiration: new Date().toISOString(), 169 | }; 170 | 171 | process.env.HOME = __dirname; 172 | 173 | aws.saveAsIniFile(credentials, 'profile', () => { 174 | const data = FS.readFileSync(awsCredentials, 'utf-8'); 175 | const config = ini.parse(data); 176 | 177 | expect(config.profile).toEqual({ 178 | aws_access_key_id: credentials.AccessKeyId, 179 | aws_secret_access_key: credentials.SecretAccessKey, 180 | aws_security_token: credentials.SessionToken, 181 | aws_session_token: credentials.SessionToken, 182 | expiration: credentials.Expiration, 183 | }); 184 | 185 | done(); 186 | }); 187 | }); 188 | 189 | it('keeps existing profiles', (done) => { 190 | const aws = new AwsCredentials(); 191 | const credentials1 = { 192 | AccessKeyId: 'AccessKeyId1', 193 | SecretAccessKey: 'SecretAccessKey1', 194 | SessionToken: 'SessionToken1', 195 | }; 196 | const credentials2 = { 197 | AccessKeyId: 'AccessKeyId2', 198 | SecretAccessKey: 'SecretAccessKey2', 199 | SessionToken: 'SessionToken2', 200 | }; 201 | const credentials3 = { 202 | AccessKeyId: 'AccessKeyId3', 203 | SecretAccessKey: 'SecretAccessKey3', 204 | SessionToken: 'SessionToken3', 205 | Expiration: new Date().toISOString(), 206 | }; 207 | 208 | process.env.HOME = __dirname; 209 | 210 | aws.saveAsIniFile(credentials1, 'profile1', () => { 211 | aws.saveAsIniFile(credentials2, 'profile2', () => { 212 | aws.saveAsIniFile(credentials3, 'profile3', () => { 213 | const data = FS.readFileSync(awsCredentials, 'utf-8'); 214 | const config = ini.parse(data); 215 | 216 | expect(config.profile1).toEqual({ 217 | aws_access_key_id: credentials1.AccessKeyId, 218 | aws_secret_access_key: credentials1.SecretAccessKey, 219 | aws_security_token: credentials1.SessionToken, 220 | aws_session_token: credentials1.SessionToken, 221 | }); 222 | expect(config.profile2).toEqual({ 223 | aws_access_key_id: credentials2.AccessKeyId, 224 | aws_secret_access_key: credentials2.SecretAccessKey, 225 | aws_security_token: credentials2.SessionToken, 226 | aws_session_token: credentials2.SessionToken, 227 | }); 228 | 229 | expect(config.profile3).toEqual({ 230 | aws_access_key_id: credentials3.AccessKeyId, 231 | aws_secret_access_key: credentials3.SecretAccessKey, 232 | aws_security_token: credentials3.SessionToken, 233 | aws_session_token: credentials3.SessionToken, 234 | expiration: credentials3.Expiration, 235 | }); 236 | 237 | done(); 238 | }); 239 | }); 240 | }); 241 | }); 242 | }); 243 | 244 | describe('AwsCredentials#resolveHomePath', () => { 245 | beforeEach(() => { 246 | delete process.env.HOME; 247 | delete process.env.USERPROFILE; 248 | delete process.env.HOMEPATH; 249 | delete process.env.HOMEDRIVE; 250 | }); 251 | 252 | it( 253 | 'returns null if $HOME, $USERPROFILE, and $HOMEPATH are undefined', 254 | () => { 255 | expect(AwsCredentials.resolveHomePath()).toBeNull(); 256 | }, 257 | ); 258 | 259 | it('uses $HOME if defined', () => { 260 | process.env.HOME = 'HOME'; 261 | 262 | expect(AwsCredentials.resolveHomePath()).toEqual('HOME'); 263 | }); 264 | 265 | it('uses $USERPROFILE if $HOME is undefined', () => { 266 | process.env.USERPROFILE = 'USERPROFILE'; 267 | 268 | expect(AwsCredentials.resolveHomePath()).toEqual('USERPROFILE'); 269 | }); 270 | 271 | it('uses $HOMEPATH if $HOME and $USERPROFILE are undefined', () => { 272 | process.env.HOMEPATH = 'HOMEPATH'; 273 | 274 | expect(AwsCredentials.resolveHomePath()).toEqual('C:/HOMEPATH'); 275 | }); 276 | 277 | it('uses $HOMEDRIVE with $HOMEPATH if defined', () => { 278 | process.env.HOMEPATH = 'HOMEPATH'; 279 | process.env.HOMEDRIVE = 'D:/'; 280 | 281 | expect(AwsCredentials.resolveHomePath()).toEqual('D:/HOMEPATH'); 282 | }); 283 | }); 284 | -------------------------------------------------------------------------------- /src/renderer/containers/refresh/Refresh.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Container, 4 | Row, 5 | Button, 6 | Collapse, 7 | Alert, 8 | } from 'reactstrap'; 9 | import { 10 | Navigate, 11 | } from 'react-router-dom'; 12 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 13 | import styled from 'styled-components'; 14 | import Error from '../components/Error'; 15 | import Logo from '../components/Logo'; 16 | import Credentials from './Credentials'; 17 | import Logout from './Logout'; 18 | import InputGroupWithCopyButton from '../components/InputGroupWithCopyButton'; 19 | import { 20 | RoundedContent, 21 | RoundedWrapper, 22 | DARK_MODE_AWARE_BORDERLESS_BUTTON, 23 | } from '../../constants/styles'; 24 | import useInterval from '../../constants/hooks'; 25 | 26 | const EnvVar = styled(RoundedContent)` 27 | margin-top: 20px; 28 | margin-bottom: 20px; 29 | padding: 10px 20px; 30 | `; 31 | 32 | const DarkModeAwareCard = styled.div` 33 | @media (prefers-color-scheme: dark) { 34 | border-color: rgb(249, 249, 249); 35 | } 36 | 37 | @media (prefers-color-scheme: light) { 38 | border-color: #333; 39 | } 40 | `; 41 | 42 | const AccountProps = styled.dl` 43 | display: grid; 44 | grid-template-columns: auto 1fr; 45 | margin: 0; 46 | padding: .5rem; 47 | `; 48 | 49 | const AccountPropsKey = styled.dt` 50 | grid-column: 1; 51 | margin-right: .5rem; 52 | `; 53 | 54 | const AccountPropsVal = styled.dd` 55 | grid-column: 2; 56 | margin-bottom: 0; 57 | `; 58 | 59 | const PreInputGroupWithCopyButton = styled(InputGroupWithCopyButton)` 60 | font-family: Consolas,monospace; 61 | font-size: 1rem; 62 | `; 63 | 64 | const BorderlessButton = styled(Button)` 65 | ${DARK_MODE_AWARE_BORDERLESS_BUTTON} 66 | `; 67 | 68 | const getLang = (platform) => (platform === 'win32' ? 'language-batch' : 'language-bash'); 69 | 70 | const getTerm = (platform) => (platform === 'win32' ? 'command prompt' : 'terminal'); 71 | 72 | const getExport = (platform) => (platform === 'win32' ? 'set' : 'export'); 73 | 74 | const getEnvVars = ({ platform, accountId }) => ` 75 | ${getExport(platform)} AWS_PROFILE=awsaml-${accountId} 76 | ${getExport(platform)} AWS_DEFAULT_PROFILE=awsaml-${accountId} 77 | `.trim(); 78 | 79 | const relativeDate = (date) => { 80 | const deltaSeconds = (new Date(date) - new Date()) / 1000; 81 | const relative = []; 82 | 83 | const hours = Math.floor(deltaSeconds / 3600); 84 | const minutes = Math.floor((deltaSeconds % 3600) / 60); 85 | const seconds = Math.floor(deltaSeconds % 60); 86 | 87 | if (hours) { 88 | relative.push(`${hours}h`); 89 | } 90 | if (minutes) { 91 | relative.push(`${minutes}m`); 92 | } else { 93 | relative.push('0m'); 94 | } 95 | 96 | if (seconds) { 97 | relative.push(`${seconds}s`); 98 | } else { 99 | relative.push('0s'); 100 | } 101 | 102 | return relative.join(' '); 103 | }; 104 | 105 | function Refresh() { 106 | const [caretDirection, setCaretDirection] = useState('down'); 107 | const [isOpen, setIsOpen] = useState(true); 108 | const [credentials, setCredentials] = useState({ 109 | accessKey: '', 110 | secretKey: '', 111 | sessionToken: '', 112 | expiration: '', 113 | }); 114 | const [accountId, setAccountId] = useState(''); 115 | const [platform, setPlatform] = useState(''); 116 | const [profileName, setProfileName] = useState(''); 117 | const [roleName, setRoleName] = useState(''); 118 | const [showRole, setShowRole] = useState(false); 119 | const [error, setError] = useState(''); 120 | const [status, setStatus] = useState(null); 121 | const [ttl, setTtl] = useState(''); 122 | const [localExpiration, setLocalExpiration] = useState(new Date()); 123 | const [darkMode, setDarkMode] = useState(false); 124 | const { accessKey, secretKey, sessionToken } = credentials; 125 | const [flash, setFlash] = useState(''); 126 | 127 | const getSuccessCallback = (data) => { 128 | const firstLoad = credentials.accessKey === ''; 129 | 130 | setAccountId(data.accountId); 131 | setCredentials({ 132 | accessKey: data.accessKey, 133 | secretKey: data.secretKey, 134 | sessionToken: data.sessionToken, 135 | expiration: data.expiration, 136 | }); 137 | 138 | setTtl(relativeDate(data.expiration)); 139 | setLocalExpiration(new Date(data.expiration)); 140 | 141 | setPlatform(data.platform); 142 | setProfileName(data.profileName); 143 | setRoleName(data.roleName); 144 | setShowRole(data.showRole); 145 | 146 | if (data.error) { 147 | setError(data.error); 148 | setFlash(''); 149 | } else { 150 | if (!firstLoad) { 151 | setFlash('Updated credentials'); 152 | setTimeout(() => { 153 | setFlash(''); 154 | }, 3000); 155 | } 156 | 157 | setError(''); 158 | } 159 | }; 160 | 161 | useInterval(() => { 162 | setTtl(relativeDate(credentials.expiration)); 163 | }, 1000); 164 | 165 | useEffect(() => { 166 | (async () => { // eslint-disable-line consistent-return 167 | const data = await window.electronAPI.refresh(); 168 | 169 | if (data.redirect) { 170 | window.location.href = data.redirect; 171 | } 172 | 173 | const dm = await window.electronAPI.getDarkMode(); 174 | setDarkMode(dm); 175 | })(); 176 | 177 | window.electronAPI.darkModeUpdated((event, value) => { 178 | setDarkMode(value); 179 | }); 180 | 181 | window.electronAPI.reloadUi((event, value) => { 182 | getSuccessCallback(value); 183 | }); 184 | 185 | return () => {}; 186 | }, []); 187 | 188 | const handleRefreshClickEvent = async (event) => { 189 | event.preventDefault(); 190 | 191 | const data = await window.electronAPI.refresh(); 192 | 193 | if (data.redirect) { 194 | window.location.href = data.redirect; 195 | } 196 | 197 | if (data.logout) { 198 | setStatus(data.logout); 199 | } 200 | }; 201 | 202 | const handleCollapse = () => { 203 | setCaretDirection(caretDirection === 'right' ? 'down' : 'right'); 204 | setIsOpen(!isOpen); 205 | }; 206 | 207 | if (status === 401) { 208 | return ; 209 | } 210 | 211 | return ( 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | {` ${flash}`} 220 | 221 | 222 |
223 | 228 | 229 | {' '} 230 |    Account 231 | 232 | 233 | 234 | 235 | {profileName !== `awsaml-${accountId}` && [ 236 | Profile:, 237 | {profileName}, 238 | ]} 239 | ID: 240 | {accountId} 241 | {showRole && [ 242 | Role:, 243 | {roleName}, 244 | ]} 245 | 246 | 247 | 248 |
249 | 255 | 256 |

257 | Run these commands from a 258 | {` ${getTerm(platform)} `} 259 | to use the AWS CLI: 260 |

261 | 270 |
271 |
272 | Expires in: 273 | {` ${ttl}`} 274 |
275 |
276 | Expires at: 277 | {` ${localExpiration.toString()}`} 278 |
279 | 280 | 287 | 288 | 289 |
290 |
291 |
292 |
293 | ); 294 | } 295 | 296 | export default Refresh; 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Awsaml 2 | 3 | [![coverage](https://coveralls.io/repos/github/rapid7/awsaml/badge.svg?branch=master)](https://coveralls.io/github/rapid7/awsaml?branch=master) 4 | 5 | Awsaml is an application for providing automatically rotated temporary [AWS][] 6 | credentials. Credentials are stored in `~/.aws/credentials` so they can be used 7 | with AWS SDKs. Credentials are valid for one hour and are rotated every hour 8 | while the application's running. 9 | 10 | In order to rotate credentials, Awsaml takes the following actions 11 | 12 | 1. Authenticates the user with their identity provider. 13 | 2. Reads the SAML authentication response returned from the identity provider. 14 | 3. Generates new temporary AWS keys by calling the [AssumeRoleWithSAML][] API.* 15 | 4. Writes the new temporary credentials to disk. 16 | 17 | This flow repeats every hour so the user always has a valid set of AWS keys 18 | while the application's running. Awsaml reuses the SAML response from the 19 | identity provider, so the user doesn't need to reauthenticate every time. 20 | 21 | You can grab prebuilt binaries for Mac, Linux, and Window from [the releases page][releases]. 22 | 23 | *This API is used to fetch credentials if the Okta SAML + AWS configuration is used. Alternatively, Awsaml also supports the Just In Time IAM tool in Rapid7's InsightCloudSec product. 24 | 25 | ## Configuration 26 | 27 | Configuring Awsaml is a multi-step process that involves a bit of back and forth 28 | between Amazon and your identity provider. The general flow looks like this 29 | 30 | 1. Create a SAML application in your identity provider. 31 | 2. Create a SAML identity provider in AWS. 32 | 3. Create an IAM role in AWS. 33 | 4. Update the SAML application with ARNs. 34 | 5. Run Awsaml and give it your application's metadata. 35 | 36 | ### 1. Create a SAML application in your identity provider 37 | 38 | The only tested identity provider is [Okta][]. To use Awsaml with Okta, you'll 39 | need to create a SAML 2.0 application in Okta with the following settings 40 | 41 | #### SAML Settings 42 | 43 | | Name | Value | 44 | |----------------------------|----------------------------------------| 45 | | Single Sign On URL | http://localhost:2600/sso/saml | 46 | | Recipient URL | http://localhost:2600/sso/saml | 47 | | Destination URL | http://localhost:2600/sso/saml | 48 | | Audience Restriction | http://localhost:2600/sso/saml | 49 | | Default Relay State | | 50 | | Name ID Format | EmailAddress | 51 | | Response | Signed | 52 | | Assertion Signature | Signed | 53 | | Signature Algorithm | RSA_SHA256 | 54 | | Digest Algorithm | SHA256 | 55 | | Assertion Encryption | Unencrypted | 56 | | SAML Single Logout | Disabled | 57 | | authnContextClassRef | PasswordProtectedTransport | 58 | | Honor Force Authentication | Yes | 59 | | SAML Issuer ID | http://www.okta.com/${org.externalKey} | 60 | 61 | Once Okta's created your application, it will show you setup instructions. 62 | 63 | Among those instructions will be a URL for a generated XML metadata document 64 | that will look something like this: 65 | 66 | ``` 67 | https://www.okta.com/app/{APP_ID}/sso/saml/metadata 68 | ``` 69 | 70 | Where `APP_ID` is the application ID Okta has assigned to your newly created 71 | app. 72 | 73 | You should do two things with this url: 74 | 75 | 1. Copy the url and store it somewhere locally because you will need to provide 76 | it to the Awsaml desktop application you run later. 77 | 2. Download the contents of the url to a file on disk because you will need to 78 | supply that file when you create an identity provider in AWS. 79 | 80 | #### A note on naming things (if you are using Okta) 81 | 82 | In the next two steps, you will create and name an identity provider and a role. 83 | Be sure to choose short names (fewer than 28 characters between the two). 84 | 85 | In the step after you create the identity provider and the role, you will need 86 | to take the ARNs for the identity provider and role and submit them to Okta. 87 | However, the field into which you will paste these values on the Okta website 88 | has a 100 character limit which is not immediately evident. 89 | 90 | You will need to provide a string in the format: 91 | 92 | ``` 93 | {ROLE_ARN},{IDENTITY_PROVIDER_ARN} 94 | ``` 95 | 96 | The `ROLE_ARN` will be in this format: 97 | 98 | ``` 99 | arn:aws:iam::{ACCOUNT_ID}:role/{ROLE_NAME} 100 | ``` 101 | 102 | Where the `ACCOUNT_ID` is 12 digits long, and the `ROLE_NAME` is as long as you 103 | want it to be. 104 | 105 | The `IDENTITY_PROVIDER_ARN` will be in this format: 106 | 107 | ``` 108 | arn:aws:iam::{ACCOUNT_ID}:saml-provider/{PROVIDER_NAME} 109 | ``` 110 | 111 | Where the `ACCOUNT_ID` is 12 digits long, and the `PROVIDER_NAME` is as long as 112 | you want it to be. 113 | 114 | Thus, when combined, the two ARNs will take up 72 characters without considering 115 | the number of characters that the names have. 116 | 117 | ``` 118 | arn:aws:iam::XXXXXXXXXXXX:role/,arn:aws:iam::XXXXXXXXXXXX:saml-provider/ 119 | ``` 120 | 121 | As a consequence, between the name you give to the identity provider and the name 122 | you give to the role, you can only use up to 28 characters. 123 | 124 | ### 2. Create a SAML identity provider in AWS 125 | 126 | Follow [Amazon's documentation for creating a SAML identity provider][saml-provider], 127 | in which you will need to upload the metadata document you downloaded in the 128 | previous step. 129 | 130 | Save the ARN for your identity provider so you can configure it in your 131 | application. 132 | 133 | ### 3. Create an IAM role in AWS 134 | 135 | Follow [Amazon's documentation for creating an IAM role][iam-role] with the 136 | following modifications: 137 | 138 | 1. In step 2 "Select Role Type" 139 | 1. After clicking "Role for Identity Provider Access", choose "Grant API 140 | access to SAML identity providers" 141 | 1. In step 3 "Establish Trust" 142 | 1. For 'SAML provider', choose the provider you previous set up 143 | 2. For 'Attribute', choose SAML:iss 144 | 3. For 'Value', supply the Issuer URL provided by Okta when you created the 145 | application 146 | 147 | The permissions in this role will be the ones users are granted by their the 148 | AWS tokens Awsaml generates. 149 | 150 | Once the role's created, a trust relationship should have been established 151 | between your role and the SAML identity provider you created. If not, you will 152 | need to set up a trust relationship between it and your SAML identity provider 153 | manually. Here's an example of the JSON policy document for that relationship. 154 | 155 | ```json 156 | { 157 | "Version": "2012-10-17", 158 | "Statement": [{ 159 | "Sid": "awsKeysSAML", 160 | "Effect": "Allow", 161 | "Principal": { 162 | "Federated": "arn:aws:iam:saml-provider" 163 | }, 164 | "Action": "sts:AssumeRoleWithSAML", 165 | "Condition": { 166 | "StringEquals": { 167 | "SAML:iss": "issuer" 168 | } 169 | } 170 | }] 171 | } 172 | ``` 173 | 174 | Replace the "issuer" value for the "SAML:iss" key in the policy document with 175 | the issuer URL for your application. Replace the "arn:aws:iam:saml-provider" 176 | value for the "Federated" key in the policy document with the ARN for your 177 | SAML identity provider. 178 | 179 | Save the ARN for the role so you can configure it in your application. 180 | 181 | ### 4. Update the SAML application with ARNs 182 | 183 | Now that you have ARNs for the AWS identity provider and role, you can go back 184 | into Okta and add them to your application. Edit your application to include the 185 | following attributes. 186 | 187 | #### Attribute Statements 188 | 189 | | Name | Name Format | Value | 190 | |--------------------------------------------------------|-------------|---------------------------------------| 191 | | https://aws.amazon.com/SAML/Attributes/Role | Unspecified | arn:aws:iam:role,arn:aws:iam:provider | 192 | | https://aws.amazon.com/SAML/Attributes/RoleSessionName | Unspecified | ${user.email} | 193 | 194 | Replace the "arn:aws:iam:role" value with the ARN of the role in AWS you 195 | created. Replace the "arn:aws:iam:provider" value with the ARN of the identity 196 | provider in AWS your created. 197 | 198 | 199 | ##### Multiple Role Support 200 | 201 | To support multiple roles, add multiple values to the `https://aws.amazon.com/SAML/Attributes/Role` 202 | attribute. For example: 203 | 204 | ``` 205 | arn:aws:iam:role1,arn:aws:iam:provider 206 | arn:aws:iam:role2,arn:aws:iam:provider 207 | arn:aws:iam:role3,arn:aws:iam:provider 208 | ``` 209 | 210 | *Special note for Okta users*: Multiple roles must be passed as multiple values to a single 211 | attribute key. By default, Okta serializes multiple values into a single value using commas. 212 | To support multiple roles, you must contact Okta support and request that the 213 | `SAML_SUPPORT_ARRAY_ATTRIBUTES` feature flag be enabled on your Okta account. For more details 214 | see [this post](https://devforum.okta.com/t/multivalued-attributes/179). 215 | 216 | 217 | ### 5. Run Awsaml and give it your application's metadata 218 | 219 | You can find a prebuilt binary for Awsaml on [the releases page][releases]. Grab 220 | the appropriate binary for your architecture and run the Awsaml application. It 221 | will prompt you for a SAML metadata URL. Enter the URL you saved in step 1. If 222 | the URL's valid, it will prompt you to log in to your identity provider. If the 223 | login's successful, you'll see temporary AWS credentials in the UI. 224 | 225 | ## Building 226 | 227 | Awsaml is built using [Node][] and [Yarn 3][], so 228 | make sure you've got a compatible versions installed. Then run Yarn to install dependencies and build Awsaml. 229 | 230 | ```bash 231 | rm -rf node_modules/ 232 | yarn install 233 | yarn build 234 | ``` 235 | 236 | Those commands will create a "out" folder with zipped binaries. If you only want to create binaries for specific platforms, you can set a `PLATFORM` environment 237 | variable before building. 238 | 239 | ```bash 240 | export PLATFORM=linux 241 | yarn build 242 | ``` 243 | 244 | Allowed values for `PLATFORM` are `darwin`, `linux` and `win32`. You can build 245 | binaries for multiple platforms by using a comma separated list. 246 | 247 | ```bash 248 | export PLATFORM=darwin,linux 249 | yarn build 250 | ``` 251 | 252 | Similarly, if you want to 253 | specify the build architecture, you can set a `ARCH` 254 | environment variable before building. 255 | 256 | ```bash 257 | export ARCH=universal 258 | export PLATFORM=darwin 259 | yarn build 260 | ``` 261 | 262 | Supported architectures are `ia32`, `x64` , `armv7l`, 263 | `arm64`, `mips64el`, `universal`, or `all`. 264 | 265 | ## Setup on macOS with Homebrew 266 | 267 | A caskfile is bundled with the repository, to install Awsaml with [Homebrew][] simply run: 268 | 269 | ``` 270 | wget https://raw.githubusercontent.com/rapid7/awsaml/master/brew/cask/awsaml.rb 271 | brew install --cask awsaml.rb 272 | ``` 273 | 274 | There might be an error and warning prompt but it should start succesfully downloading right after 275 | When download is succesfully installed, a `awsaml was successfully installed!` prompt is displayed 276 | 277 | ## License 278 | 279 | Awsaml is licensed under a MIT License. See the "LICENSE.md" file for more 280 | details. 281 | 282 | ## Special Thanks 283 | 284 | * [Tristan Harward] for the app icon. 285 | 286 | [AWS]: https://aws.amazon.com 287 | [AssumeRoleWithSAML]: http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html 288 | [releases]: https://github.com/rapid7/awsaml/releases 289 | [Okta]: https://www.okta.com 290 | [Node]: https://nodejs.org 291 | [Yarn 3]: https://yarnpkg.com 292 | [saml-provider]: http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml.html 293 | [iam-role]: http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_saml.html 294 | [Homebrew]: http://brew.sh/ 295 | [Tristan Harward]: https://github.com/trisweb 296 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-outdated.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-outdated", 5 | factory: function (require) { 6 | var plugin=(()=>{var ur=Object.create,_t=Object.defineProperty,hr=Object.defineProperties,pr=Object.getOwnPropertyDescriptor,fr=Object.getOwnPropertyDescriptors,dr=Object.getOwnPropertyNames,xe=Object.getOwnPropertySymbols,gr=Object.getPrototypeOf,Ce=Object.prototype.hasOwnProperty,mr=Object.prototype.propertyIsEnumerable;var Ee=(e,t,s)=>t in e?_t(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s,E=(e,t)=>{for(var s in t||(t={}))Ce.call(t,s)&&Ee(e,s,t[s]);if(xe)for(var s of xe(t))mr.call(t,s)&&Ee(e,s,t[s]);return e},k=(e,t)=>hr(e,fr(t)),yr=e=>_t(e,"__esModule",{value:!0});var q=e=>{if(typeof require!="undefined")return require(e);throw new Error('Dynamic require of "'+e+'" is not supported')};var M=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Ar=(e,t)=>{for(var s in t)_t(e,s,{get:t[s],enumerable:!0})},Rr=(e,t,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of dr(t))!Ce.call(e,r)&&r!=="default"&&_t(e,r,{get:()=>t[r],enumerable:!(s=pr(t,r))||s.enumerable});return e},J=e=>Rr(yr(_t(e!=null?ur(gr(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var Nt=M(st=>{"use strict";st.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;st.find=(e,t)=>e.nodes.find(s=>s.type===t);st.exceedsLimit=(e,t,s=1,r)=>r===!1||!st.isInteger(e)||!st.isInteger(t)?!1:(Number(t)-Number(e))/Number(s)>=r;st.escapeNode=(e,t=0,s)=>{let r=e.nodes[t];!r||(s&&r.type===s||r.type==="open"||r.type==="close")&&r.escaped!==!0&&(r.value="\\"+r.value,r.escaped=!0)};st.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0==0?(e.invalid=!0,!0):!1;st.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0==0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;st.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;st.reduce=e=>e.reduce((t,s)=>(s.type==="text"&&t.push(s.value),s.type==="range"&&(s.type="text"),t),[]);st.flatten=(...e)=>{let t=[],s=r=>{for(let i=0;i{"use strict";var Se=Nt();$e.exports=(e,t={})=>{let s=(r,i={})=>{let n=t.escapeInvalid&&Se.isInvalidBrace(i),o=r.invalid===!0&&t.escapeInvalid===!0,a="";if(r.value)return(n||o)&&Se.isOpenOrClose(r)?"\\"+r.value:r.value;if(r.value)return r.value;if(r.nodes)for(let d of r.nodes)a+=s(d);return a};return s(e)}});var ve=M((en,we)=>{"use strict";we.exports=function(e){return typeof e=="number"?e-e==0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var Me=M((sn,De)=>{"use strict";var Te=ve(),dt=(e,t,s)=>{if(Te(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(t===void 0||e===t)return String(e);if(Te(t)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let r=E({relaxZeros:!0},s);typeof r.strictZeros=="boolean"&&(r.relaxZeros=r.strictZeros===!1);let i=String(r.relaxZeros),n=String(r.shorthand),o=String(r.capture),a=String(r.wrap),d=e+":"+t+"="+i+n+o+a;if(dt.cache.hasOwnProperty(d))return dt.cache[d].result;let f=Math.min(e,t),h=Math.max(e,t);if(Math.abs(f-h)===1){let R=e+"|"+t;return r.capture?`(${R})`:r.wrap===!1?R:`(?:${R})`}let g=Ne(e)||Ne(t),l={min:e,max:t,a:f,b:h},_=[],A=[];if(g&&(l.isPadded=g,l.maxLen=String(l.max).length),f<0){let R=h<0?Math.abs(h):1;A=He(R,Math.abs(f),l,r),f=l.a=0}return h>=0&&(_=He(f,h,l,r)),l.negatives=A,l.positives=_,l.result=_r(A,_,r),r.capture===!0?l.result=`(${l.result})`:r.wrap!==!1&&_.length+A.length>1&&(l.result=`(?:${l.result})`),dt.cache[d]=l,l.result};function _r(e,t,s){let r=Vt(e,t,"-",!1,s)||[],i=Vt(t,e,"",!1,s)||[],n=Vt(e,t,"-?",!0,s)||[];return r.concat(n).concat(i).join("|")}function br(e,t){let s=1,r=1,i=Oe(e,s),n=new Set([t]);for(;e<=i&&i<=t;)n.add(i),s+=1,i=Oe(e,s);for(i=ke(t+1,r)-1;e1&&a.count.pop(),a.count.push(h.count[0]),a.string=a.pattern+Ie(a.count),o=f+1;continue}s.isPadded&&(g=$r(f,s,r)),h.string=g+h.pattern+Ie(h.count),n.push(h),o=f+1,a=h}return n}function Vt(e,t,s,r,i){let n=[];for(let o of e){let{string:a}=o;!r&&!Le(t,"string",a)&&n.push(s+a),r&&Le(t,"string",a)&&n.push(s+a)}return n}function Cr(e,t){let s=[];for(let r=0;rt?1:t>e?-1:0}function Le(e,t,s){return e.some(r=>r[t]===s)}function Oe(e,t){return Number(String(e).slice(0,-t)+"9".repeat(t))}function ke(e,t){return e-e%Math.pow(10,t)}function Ie(e){let[t=0,s=""]=e;return s||t>1?`{${t+(s?","+s:"")}}`:""}function Sr(e,t,s){return`[${e}${t-e==1?"":"-"}${t}]`}function Ne(e){return/^-?(0+)\d/.test(e)}function $r(e,t,s){if(!t.isPadded)return e;let r=Math.abs(t.maxLen-String(e).length),i=s.relaxZeros!==!1;switch(r){case 0:return"";case 1:return i?"0?":"0";case 2:return i?"0{0,2}":"00";default:return i?`0{0,${r}}`:`0{${r}}`}}dt.cache={};dt.clearCache=()=>dt.cache={};De.exports=dt});var Jt=M((rn,Ge)=>{"use strict";var wr=q("util"),Pe=Me(),Ue=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),vr=e=>t=>e===!0?Number(t):String(t),Zt=e=>typeof e=="number"||typeof e=="string"&&e!=="",bt=e=>Number.isInteger(+e),Yt=e=>{let t=`${e}`,s=-1;if(t[0]==="-"&&(t=t.slice(1)),t==="0")return!1;for(;t[++s]==="0";);return s>0},Tr=(e,t,s)=>typeof e=="string"||typeof t=="string"?!0:s.stringify===!0,Hr=(e,t,s)=>{if(t>0){let r=e[0]==="-"?"-":"";r&&(e=e.slice(1)),e=r+e.padStart(r?t-1:t,"0")}return s===!1?String(e):e},ze=(e,t)=>{let s=e[0]==="-"?"-":"";for(s&&(e=e.slice(1),t--);e.length{e.negatives.sort((o,a)=>oa?1:0),e.positives.sort((o,a)=>oa?1:0);let s=t.capture?"":"?:",r="",i="",n;return e.positives.length&&(r=e.positives.join("|")),e.negatives.length&&(i=`-(${s}${e.negatives.join("|")})`),r&&i?n=`${r}|${i}`:n=r||i,t.wrap?`(${s}${n})`:n},Be=(e,t,s,r)=>{if(s)return Pe(e,t,E({wrap:!1},r));let i=String.fromCharCode(e);if(e===t)return i;let n=String.fromCharCode(t);return`[${i}-${n}]`},je=(e,t,s)=>{if(Array.isArray(e)){let r=s.wrap===!0,i=s.capture?"":"?:";return r?`(${i}${e.join("|")})`:e.join("|")}return Pe(e,t,s)},Fe=(...e)=>new RangeError("Invalid range arguments: "+wr.inspect(...e)),We=(e,t,s)=>{if(s.strictRanges===!0)throw Fe([e,t]);return[]},Or=(e,t)=>{if(t.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},kr=(e,t,s=1,r={})=>{let i=Number(e),n=Number(t);if(!Number.isInteger(i)||!Number.isInteger(n)){if(r.strictRanges===!0)throw Fe([e,t]);return[]}i===0&&(i=0),n===0&&(n=0);let o=i>n,a=String(e),d=String(t),f=String(s);s=Math.max(Math.abs(s),1);let h=Yt(a)||Yt(d)||Yt(f),g=h?Math.max(a.length,d.length,f.length):0,l=h===!1&&Tr(e,t,r)===!1,_=r.transform||vr(l);if(r.toRegex&&s===1)return Be(ze(e,g),ze(t,g),!0,r);let A={negatives:[],positives:[]},R=H=>A[H<0?"negatives":"positives"].push(Math.abs(H)),x=[],$=0;for(;o?i>=n:i<=n;)r.toRegex===!0&&s>1?R(i):x.push(Hr(_(i,$),g,l)),i=o?i-s:i+s,$++;return r.toRegex===!0?s>1?Lr(A,r):je(x,null,E({wrap:!1},r)):x},Ir=(e,t,s=1,r={})=>{if(!bt(e)&&e.length>1||!bt(t)&&t.length>1)return We(e,t,r);let i=r.transform||(l=>String.fromCharCode(l)),n=`${e}`.charCodeAt(0),o=`${t}`.charCodeAt(0),a=n>o,d=Math.min(n,o),f=Math.max(n,o);if(r.toRegex&&s===1)return Be(d,f,!1,r);let h=[],g=0;for(;a?n>=o:n<=o;)h.push(i(n,g)),n=a?n-s:n+s,g++;return r.toRegex===!0?je(h,null,{wrap:!1,options:r}):h},Mt=(e,t,s,r={})=>{if(t==null&&Zt(e))return[e];if(!Zt(e)||!Zt(t))return We(e,t,r);if(typeof s=="function")return Mt(e,t,1,{transform:s});if(Ue(s))return Mt(e,t,0,s);let i=E({},r);return i.capture===!0&&(i.wrap=!0),s=s||i.step||1,bt(s)?bt(e)&&bt(t)?kr(e,t,s,i):Ir(e,t,Math.max(Math.abs(s),1),i):s!=null&&!Ue(s)?Or(s,i):Mt(e,t,1,s)};Ge.exports=Mt});var Qe=M((nn,Ke)=>{"use strict";var Nr=Jt(),qe=Nt(),Dr=(e,t={})=>{let s=(r,i={})=>{let n=qe.isInvalidBrace(i),o=r.invalid===!0&&t.escapeInvalid===!0,a=n===!0||o===!0,d=t.escapeInvalid===!0?"\\":"",f="";if(r.isOpen===!0||r.isClose===!0)return d+r.value;if(r.type==="open")return a?d+r.value:"(";if(r.type==="close")return a?d+r.value:")";if(r.type==="comma")return r.prev.type==="comma"?"":a?r.value:"|";if(r.value)return r.value;if(r.nodes&&r.ranges>0){let h=qe.reduce(r.nodes),g=Nr(...h,k(E({},t),{wrap:!1,toRegex:!0}));if(g.length!==0)return h.length>1&&g.length>1?`(${g})`:g}if(r.nodes)for(let h of r.nodes)f+=s(h,r);return f};return s(e)};Ke.exports=Dr});var Ze=M((on,Ve)=>{"use strict";var Mr=Jt(),Xe=Dt(),yt=Nt(),gt=(e="",t="",s=!1)=>{let r=[];if(e=[].concat(e),t=[].concat(t),!t.length)return e;if(!e.length)return s?yt.flatten(t).map(i=>`{${i}}`):t;for(let i of e)if(Array.isArray(i))for(let n of i)r.push(gt(n,t,s));else for(let n of t)s===!0&&typeof n=="string"&&(n=`{${n}}`),r.push(Array.isArray(n)?gt(i,n,s):i+n);return yt.flatten(r)},Pr=(e,t={})=>{let s=t.rangeLimit===void 0?1e3:t.rangeLimit,r=(i,n={})=>{i.queue=[];let o=n,a=n.queue;for(;o.type!=="brace"&&o.type!=="root"&&o.parent;)o=o.parent,a=o.queue;if(i.invalid||i.dollar){a.push(gt(a.pop(),Xe(i,t)));return}if(i.type==="brace"&&i.invalid!==!0&&i.nodes.length===2){a.push(gt(a.pop(),["{}"]));return}if(i.nodes&&i.ranges>0){let g=yt.reduce(i.nodes);if(yt.exceedsLimit(...g,t.step,s))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let l=Mr(...g,t);l.length===0&&(l=Xe(i,t)),a.push(gt(a.pop(),l)),i.nodes=[];return}let d=yt.encloseBrace(i),f=i.queue,h=i;for(;h.type!=="brace"&&h.type!=="root"&&h.parent;)h=h.parent,f=h.queue;for(let g=0;g{"use strict";Ye.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` 7 | `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var is=M((ln,rs)=>{"use strict";var Ur=Dt(),{MAX_LENGTH:ts,CHAR_BACKSLASH:te,CHAR_BACKTICK:zr,CHAR_COMMA:Br,CHAR_DOT:jr,CHAR_LEFT_PARENTHESES:Fr,CHAR_RIGHT_PARENTHESES:Wr,CHAR_LEFT_CURLY_BRACE:Gr,CHAR_RIGHT_CURLY_BRACE:qr,CHAR_LEFT_SQUARE_BRACKET:es,CHAR_RIGHT_SQUARE_BRACKET:ss,CHAR_DOUBLE_QUOTE:Kr,CHAR_SINGLE_QUOTE:Qr,CHAR_NO_BREAK_SPACE:Xr,CHAR_ZERO_WIDTH_NOBREAK_SPACE:Vr}=Je(),Zr=(e,t={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let s=t||{},r=typeof s.maxLength=="number"?Math.min(ts,s.maxLength):ts;if(e.length>r)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${r})`);let i={type:"root",input:e,nodes:[]},n=[i],o=i,a=i,d=0,f=e.length,h=0,g=0,l,_={},A=()=>e[h++],R=x=>{if(x.type==="text"&&a.type==="dot"&&(a.type="text"),a&&a.type==="text"&&x.type==="text"){a.value+=x.value;return}return o.nodes.push(x),x.parent=o,x.prev=a,a=x,x};for(R({type:"bos"});h0){if(o.ranges>0){o.ranges=0;let x=o.nodes.shift();o.nodes=[x,{type:"text",value:Ur(o)}]}R({type:"comma",value:l}),o.commas++;continue}if(l===jr&&g>0&&o.commas===0){let x=o.nodes;if(g===0||x.length===0){R({type:"text",value:l});continue}if(a.type==="dot"){if(o.range=[],a.value+=l,a.type="range",o.nodes.length!==3&&o.nodes.length!==5){o.invalid=!0,o.ranges=0,a.type="text";continue}o.ranges++,o.args=[];continue}if(a.type==="range"){x.pop();let $=x[x.length-1];$.value+=a.value+l,a=$,o.ranges--;continue}R({type:"dot",value:l});continue}R({type:"text",value:l})}do if(o=n.pop(),o.type!=="root"){o.nodes.forEach(H=>{H.nodes||(H.type==="open"&&(H.isOpen=!0),H.type==="close"&&(H.isClose=!0),H.nodes||(H.type="text"),H.invalid=!0)});let x=n[n.length-1],$=x.nodes.indexOf(o);x.nodes.splice($,1,...o.nodes)}while(n.length>0);return R({type:"eos"}),i};rs.exports=Zr});var as=M((cn,os)=>{"use strict";var ns=Dt(),Yr=Qe(),Jr=Ze(),ti=is(),tt=(e,t={})=>{let s=[];if(Array.isArray(e))for(let r of e){let i=tt.create(r,t);Array.isArray(i)?s.push(...i):s.push(i)}else s=[].concat(tt.create(e,t));return t&&t.expand===!0&&t.nodupes===!0&&(s=[...new Set(s)]),s};tt.parse=(e,t={})=>ti(e,t);tt.stringify=(e,t={})=>typeof e=="string"?ns(tt.parse(e,t),t):ns(e,t);tt.compile=(e,t={})=>(typeof e=="string"&&(e=tt.parse(e,t)),Yr(e,t));tt.expand=(e,t={})=>{typeof e=="string"&&(e=tt.parse(e,t));let s=Jr(e,t);return t.noempty===!0&&(s=s.filter(Boolean)),t.nodupes===!0&&(s=[...new Set(s)]),s};tt.create=(e,t={})=>e===""||e.length<3?[e]:t.expand!==!0?tt.compile(e,t):tt.expand(e,t);os.exports=tt});var xt=M((un,ps)=>{"use strict";var ei=q("path"),at="\\\\/",ls=`[^${at}]`,ct="\\.",si="\\+",ri="\\?",Pt="\\/",ii="(?=.)",cs="[^/]",ee=`(?:${Pt}|$)`,us=`(?:^|${Pt})`,se=`${ct}{1,2}${ee}`,ni=`(?!${ct})`,oi=`(?!${us}${se})`,ai=`(?!${ct}{0,1}${ee})`,li=`(?!${se})`,ci=`[^.${Pt}]`,ui=`${cs}*?`,hs={DOT_LITERAL:ct,PLUS_LITERAL:si,QMARK_LITERAL:ri,SLASH_LITERAL:Pt,ONE_CHAR:ii,QMARK:cs,END_ANCHOR:ee,DOTS_SLASH:se,NO_DOT:ni,NO_DOTS:oi,NO_DOT_SLASH:ai,NO_DOTS_SLASH:li,QMARK_NO_DOT:ci,STAR:ui,START_ANCHOR:us},hi=k(E({},hs),{SLASH_LITERAL:`[${at}]`,QMARK:ls,STAR:`${ls}*?`,DOTS_SLASH:`${ct}{1,2}(?:[${at}]|$)`,NO_DOT:`(?!${ct})`,NO_DOTS:`(?!(?:^|[${at}])${ct}{1,2}(?:[${at}]|$))`,NO_DOT_SLASH:`(?!${ct}{0,1}(?:[${at}]|$))`,NO_DOTS_SLASH:`(?!${ct}{1,2}(?:[${at}]|$))`,QMARK_NO_DOT:`[^.${at}]`,START_ANCHOR:`(?:^|[${at}])`,END_ANCHOR:`(?:[${at}]|$)`}),pi={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};ps.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:pi,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:ei.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?hi:hs}}});var Ct=M(X=>{"use strict";var fi=q("path"),di=process.platform==="win32",{REGEX_BACKSLASH:gi,REGEX_REMOVE_BACKSLASH:mi,REGEX_SPECIAL_CHARS:yi,REGEX_SPECIAL_CHARS_GLOBAL:Ai}=xt();X.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);X.hasRegexChars=e=>yi.test(e);X.isRegexChar=e=>e.length===1&&X.hasRegexChars(e);X.escapeRegex=e=>e.replace(Ai,"\\$1");X.toPosixSlashes=e=>e.replace(gi,"/");X.removeBackslashes=e=>e.replace(mi,t=>t==="\\"?"":t);X.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};X.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:di===!0||fi.sep==="\\";X.escapeLast=(e,t,s)=>{let r=e.lastIndexOf(t,s);return r===-1?e:e[r-1]==="\\"?X.escapeLast(e,t,r-1):`${e.slice(0,r)}\\${e.slice(r)}`};X.removePrefix=(e,t={})=>{let s=e;return s.startsWith("./")&&(s=s.slice(2),t.prefix="./"),s};X.wrapOutput=(e,t={},s={})=>{let r=s.contains?"":"^",i=s.contains?"":"$",n=`${r}(?:${e})${i}`;return t.negated===!0&&(n=`(?:^(?!${n}).*$)`),n}});var _s=M((pn,Rs)=>{"use strict";var fs=Ct(),{CHAR_ASTERISK:re,CHAR_AT:Ri,CHAR_BACKWARD_SLASH:Et,CHAR_COMMA:_i,CHAR_DOT:ie,CHAR_EXCLAMATION_MARK:ne,CHAR_FORWARD_SLASH:ds,CHAR_LEFT_CURLY_BRACE:oe,CHAR_LEFT_PARENTHESES:ae,CHAR_LEFT_SQUARE_BRACKET:bi,CHAR_PLUS:xi,CHAR_QUESTION_MARK:gs,CHAR_RIGHT_CURLY_BRACE:Ci,CHAR_RIGHT_PARENTHESES:ms,CHAR_RIGHT_SQUARE_BRACKET:Ei}=xt(),ys=e=>e===ds||e===Et,As=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?Infinity:1)},Si=(e,t)=>{let s=t||{},r=e.length-1,i=s.parts===!0||s.scanToEnd===!0,n=[],o=[],a=[],d=e,f=-1,h=0,g=0,l=!1,_=!1,A=!1,R=!1,x=!1,$=!1,H=!1,I=!1,Z=!1,j=!1,it=0,F,b,T={value:"",depth:0,isGlob:!1},G=()=>f>=r,p=()=>d.charCodeAt(f+1),N=()=>(F=b,d.charCodeAt(++f));for(;f0&&(ut=d.slice(0,h),d=d.slice(h),g-=h),L&&A===!0&&g>0?(L=d.slice(0,g),c=d.slice(g)):A===!0?(L="",c=d):L=d,L&&L!==""&&L!=="/"&&L!==d&&ys(L.charCodeAt(L.length-1))&&(L=L.slice(0,-1)),s.unescape===!0&&(c&&(c=fs.removeBackslashes(c)),L&&H===!0&&(L=fs.removeBackslashes(L)));let u={prefix:ut,input:e,start:h,base:L,glob:c,isBrace:l,isBracket:_,isGlob:A,isExtglob:R,isGlobstar:x,negated:I,negatedExtglob:Z};if(s.tokens===!0&&(u.maxDepth=0,ys(b)||o.push(T),u.tokens=o),s.parts===!0||s.tokens===!0){let K;for(let w=0;w{"use strict";var Ut=xt(),et=Ct(),{MAX_LENGTH:zt,POSIX_REGEX_SOURCE:$i,REGEX_NON_SPECIAL_CHARS:wi,REGEX_SPECIAL_CHARS_BACKREF:vi,REPLACEMENTS:bs}=Ut,Ti=(e,t)=>{if(typeof t.expandRange=="function")return t.expandRange(...e,t);e.sort();let s=`[${e.join("-")}]`;try{new RegExp(s)}catch(r){return e.map(i=>et.escapeRegex(i)).join("..")}return s},At=(e,t)=>`Missing ${e}: "${t}" - use "\\\\${t}" to match literal characters`,xs=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=bs[e]||e;let s=E({},t),r=typeof s.maxLength=="number"?Math.min(zt,s.maxLength):zt,i=e.length;if(i>r)throw new SyntaxError(`Input length: ${i}, exceeds maximum allowed length: ${r}`);let n={type:"bos",value:"",output:s.prepend||""},o=[n],a=s.capture?"":"?:",d=et.isWindows(t),f=Ut.globChars(d),h=Ut.extglobChars(f),{DOT_LITERAL:g,PLUS_LITERAL:l,SLASH_LITERAL:_,ONE_CHAR:A,DOTS_SLASH:R,NO_DOT:x,NO_DOT_SLASH:$,NO_DOTS_SLASH:H,QMARK:I,QMARK_NO_DOT:Z,STAR:j,START_ANCHOR:it}=f,F=y=>`(${a}(?:(?!${it}${y.dot?R:g}).)*?)`,b=s.dot?"":x,T=s.dot?I:Z,G=s.bash===!0?F(s):j;s.capture&&(G=`(${G})`),typeof s.noext=="boolean"&&(s.noextglob=s.noext);let p={input:e,index:-1,start:0,dot:s.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:o};e=et.removePrefix(e,p),i=e.length;let N=[],L=[],ut=[],c=n,u,K=()=>p.index===i-1,w=p.peek=(y=1)=>e[p.index+y],nt=p.advance=()=>e[++p.index]||"",ot=()=>e.slice(p.index+1),Y=(y="",O=0)=>{p.consumed+=y,p.index+=O},Lt=y=>{p.output+=y.output!=null?y.output:y.value,Y(y.value)},lr=()=>{let y=1;for(;w()==="!"&&(w(2)!=="("||w(3)==="?");)nt(),p.start++,y++;return y%2==0?!1:(p.negated=!0,p.start++,!0)},Ot=y=>{p[y]++,ut.push(y)},ft=y=>{p[y]--,ut.pop()},S=y=>{if(c.type==="globstar"){let O=p.braces>0&&(y.type==="comma"||y.type==="brace"),m=y.extglob===!0||N.length&&(y.type==="pipe"||y.type==="paren");y.type!=="slash"&&y.type!=="paren"&&!O&&!m&&(p.output=p.output.slice(0,-c.output.length),c.type="star",c.value="*",c.output=G,p.output+=c.output)}if(N.length&&y.type!=="paren"&&(N[N.length-1].inner+=y.value),(y.value||y.output)&&Lt(y),c&&c.type==="text"&&y.type==="text"){c.value+=y.value,c.output=(c.output||"")+y.value;return}y.prev=c,o.push(y),c=y},kt=(y,O)=>{let m=k(E({},h[O]),{conditions:1,inner:""});m.prev=c,m.parens=p.parens,m.output=p.output;let C=(s.capture?"(":"")+m.open;Ot("parens"),S({type:y,value:O,output:p.output?"":A}),S({type:"paren",extglob:!0,value:nt(),output:C}),N.push(m)},cr=y=>{let O=y.close+(s.capture?")":""),m;if(y.type==="negate"){let C=G;y.inner&&y.inner.length>1&&y.inner.includes("/")&&(C=F(s)),(C!==G||K()||/^\)+$/.test(ot()))&&(O=y.close=`)$))${C}`),y.inner.includes("*")&&(m=ot())&&/^\.[^\\/.]+$/.test(m)&&(O=y.close=`)${m})${C})`),y.prev.type==="bos"&&(p.negatedExtglob=!0)}S({type:"paren",extglob:!0,value:u,output:O}),ft("parens")};if(s.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let y=!1,O=e.replace(vi,(m,C,U,Q,W,Xt)=>Q==="\\"?(y=!0,m):Q==="?"?C?C+Q+(W?I.repeat(W.length):""):Xt===0?T+(W?I.repeat(W.length):""):I.repeat(U.length):Q==="."?g.repeat(U.length):Q==="*"?C?C+Q+(W?G:""):G:C?m:`\\${m}`);return y===!0&&(s.unescape===!0?O=O.replace(/\\/g,""):O=O.replace(/\\+/g,m=>m.length%2==0?"\\\\":m?"\\":"")),O===e&&s.contains===!0?(p.output=e,p):(p.output=et.wrapOutput(O,p,t),p)}for(;!K();){if(u=nt(),u==="\0")continue;if(u==="\\"){let m=w();if(m==="/"&&s.bash!==!0||m==="."||m===";")continue;if(!m){u+="\\",S({type:"text",value:u});continue}let C=/^\\+/.exec(ot()),U=0;if(C&&C[0].length>2&&(U=C[0].length,p.index+=U,U%2!=0&&(u+="\\")),s.unescape===!0?u=nt():u+=nt(),p.brackets===0){S({type:"text",value:u});continue}}if(p.brackets>0&&(u!=="]"||c.value==="["||c.value==="[^")){if(s.posix!==!1&&u===":"){let m=c.value.slice(1);if(m.includes("[")&&(c.posix=!0,m.includes(":"))){let C=c.value.lastIndexOf("["),U=c.value.slice(0,C),Q=c.value.slice(C+2),W=$i[Q];if(W){c.value=U+W,p.backtrack=!0,nt(),!n.output&&o.indexOf(c)===1&&(n.output=A);continue}}}(u==="["&&w()!==":"||u==="-"&&w()==="]")&&(u=`\\${u}`),u==="]"&&(c.value==="["||c.value==="[^")&&(u=`\\${u}`),s.posix===!0&&u==="!"&&c.value==="["&&(u="^"),c.value+=u,Lt({value:u});continue}if(p.quotes===1&&u!=='"'){u=et.escapeRegex(u),c.value+=u,Lt({value:u});continue}if(u==='"'){p.quotes=p.quotes===1?0:1,s.keepQuotes===!0&&S({type:"text",value:u});continue}if(u==="("){Ot("parens"),S({type:"paren",value:u});continue}if(u===")"){if(p.parens===0&&s.strictBrackets===!0)throw new SyntaxError(At("opening","("));let m=N[N.length-1];if(m&&p.parens===m.parens+1){cr(N.pop());continue}S({type:"paren",value:u,output:p.parens?")":"\\)"}),ft("parens");continue}if(u==="["){if(s.nobracket===!0||!ot().includes("]")){if(s.nobracket!==!0&&s.strictBrackets===!0)throw new SyntaxError(At("closing","]"));u=`\\${u}`}else Ot("brackets");S({type:"bracket",value:u});continue}if(u==="]"){if(s.nobracket===!0||c&&c.type==="bracket"&&c.value.length===1){S({type:"text",value:u,output:`\\${u}`});continue}if(p.brackets===0){if(s.strictBrackets===!0)throw new SyntaxError(At("opening","["));S({type:"text",value:u,output:`\\${u}`});continue}ft("brackets");let m=c.value.slice(1);if(c.posix!==!0&&m[0]==="^"&&!m.includes("/")&&(u=`/${u}`),c.value+=u,Lt({value:u}),s.literalBrackets===!1||et.hasRegexChars(m))continue;let C=et.escapeRegex(c.value);if(p.output=p.output.slice(0,-c.value.length),s.literalBrackets===!0){p.output+=C,c.value=C;continue}c.value=`(${a}${C}|${c.value})`,p.output+=c.value;continue}if(u==="{"&&s.nobrace!==!0){Ot("braces");let m={type:"brace",value:u,output:"(",outputIndex:p.output.length,tokensIndex:p.tokens.length};L.push(m),S(m);continue}if(u==="}"){let m=L[L.length-1];if(s.nobrace===!0||!m){S({type:"text",value:u,output:u});continue}let C=")";if(m.dots===!0){let U=o.slice(),Q=[];for(let W=U.length-1;W>=0&&(o.pop(),U[W].type!=="brace");W--)U[W].type!=="dots"&&Q.unshift(U[W].value);C=Ti(Q,s),p.backtrack=!0}if(m.comma!==!0&&m.dots!==!0){let U=p.output.slice(0,m.outputIndex),Q=p.tokens.slice(m.tokensIndex);m.value=m.output="\\{",u=C="\\}",p.output=U;for(let W of Q)p.output+=W.output||W.value}S({type:"brace",value:u,output:C}),ft("braces"),L.pop();continue}if(u==="|"){N.length>0&&N[N.length-1].conditions++,S({type:"text",value:u});continue}if(u===","){let m=u,C=L[L.length-1];C&&ut[ut.length-1]==="braces"&&(C.comma=!0,m="|"),S({type:"comma",value:u,output:m});continue}if(u==="/"){if(c.type==="dot"&&p.index===p.start+1){p.start=p.index+1,p.consumed="",p.output="",o.pop(),c=n;continue}S({type:"slash",value:u,output:_});continue}if(u==="."){if(p.braces>0&&c.type==="dot"){c.value==="."&&(c.output=g);let m=L[L.length-1];c.type="dots",c.output+=u,c.value+=u,m.dots=!0;continue}if(p.braces+p.parens===0&&c.type!=="bos"&&c.type!=="slash"){S({type:"text",value:u,output:g});continue}S({type:"dot",value:u,output:g});continue}if(u==="?"){if(!(c&&c.value==="(")&&s.noextglob!==!0&&w()==="("&&w(2)!=="?"){kt("qmark",u);continue}if(c&&c.type==="paren"){let C=w(),U=u;if(C==="<"&&!et.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(c.value==="("&&!/[!=<:]/.test(C)||C==="<"&&!/<([!=]|\w+>)/.test(ot()))&&(U=`\\${u}`),S({type:"text",value:u,output:U});continue}if(s.dot!==!0&&(c.type==="slash"||c.type==="bos")){S({type:"qmark",value:u,output:Z});continue}S({type:"qmark",value:u,output:I});continue}if(u==="!"){if(s.noextglob!==!0&&w()==="("&&(w(2)!=="?"||!/[!=<:]/.test(w(3)))){kt("negate",u);continue}if(s.nonegate!==!0&&p.index===0){lr();continue}}if(u==="+"){if(s.noextglob!==!0&&w()==="("&&w(2)!=="?"){kt("plus",u);continue}if(c&&c.value==="("||s.regex===!1){S({type:"plus",value:u,output:l});continue}if(c&&(c.type==="bracket"||c.type==="paren"||c.type==="brace")||p.parens>0){S({type:"plus",value:u});continue}S({type:"plus",value:l});continue}if(u==="@"){if(s.noextglob!==!0&&w()==="("&&w(2)!=="?"){S({type:"at",extglob:!0,value:u,output:""});continue}S({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let m=wi.exec(ot());m&&(u+=m[0],p.index+=m[0].length),S({type:"text",value:u});continue}if(c&&(c.type==="globstar"||c.star===!0)){c.type="star",c.star=!0,c.value+=u,c.output=G,p.backtrack=!0,p.globstar=!0,Y(u);continue}let y=ot();if(s.noextglob!==!0&&/^\([^?]/.test(y)){kt("star",u);continue}if(c.type==="star"){if(s.noglobstar===!0){Y(u);continue}let m=c.prev,C=m.prev,U=m.type==="slash"||m.type==="bos",Q=C&&(C.type==="star"||C.type==="globstar");if(s.bash===!0&&(!U||y[0]&&y[0]!=="/")){S({type:"star",value:u,output:""});continue}let W=p.braces>0&&(m.type==="comma"||m.type==="brace"),Xt=N.length&&(m.type==="pipe"||m.type==="paren");if(!U&&m.type!=="paren"&&!W&&!Xt){S({type:"star",value:u,output:""});continue}for(;y.slice(0,3)==="/**";){let It=e[p.index+4];if(It&&It!=="/")break;y=y.slice(3),Y("/**",3)}if(m.type==="bos"&&K()){c.type="globstar",c.value+=u,c.output=F(s),p.output=c.output,p.globstar=!0,Y(u);continue}if(m.type==="slash"&&m.prev.type!=="bos"&&!Q&&K()){p.output=p.output.slice(0,-(m.output+c.output).length),m.output=`(?:${m.output}`,c.type="globstar",c.output=F(s)+(s.strictSlashes?")":"|$)"),c.value+=u,p.globstar=!0,p.output+=m.output+c.output,Y(u);continue}if(m.type==="slash"&&m.prev.type!=="bos"&&y[0]==="/"){let It=y[1]!==void 0?"|$":"";p.output=p.output.slice(0,-(m.output+c.output).length),m.output=`(?:${m.output}`,c.type="globstar",c.output=`${F(s)}${_}|${_}${It})`,c.value+=u,p.output+=m.output+c.output,p.globstar=!0,Y(u+nt()),S({type:"slash",value:"/",output:""});continue}if(m.type==="bos"&&y[0]==="/"){c.type="globstar",c.value+=u,c.output=`(?:^|${_}|${F(s)}${_})`,p.output=c.output,p.globstar=!0,Y(u+nt()),S({type:"slash",value:"/",output:""});continue}p.output=p.output.slice(0,-c.output.length),c.type="globstar",c.output=F(s),c.value+=u,p.output+=c.output,p.globstar=!0,Y(u);continue}let O={type:"star",value:u,output:G};if(s.bash===!0){O.output=".*?",(c.type==="bos"||c.type==="slash")&&(O.output=b+O.output),S(O);continue}if(c&&(c.type==="bracket"||c.type==="paren")&&s.regex===!0){O.output=u,S(O);continue}(p.index===p.start||c.type==="slash"||c.type==="dot")&&(c.type==="dot"?(p.output+=$,c.output+=$):s.dot===!0?(p.output+=H,c.output+=H):(p.output+=b,c.output+=b),w()!=="*"&&(p.output+=A,c.output+=A)),S(O)}for(;p.brackets>0;){if(s.strictBrackets===!0)throw new SyntaxError(At("closing","]"));p.output=et.escapeLast(p.output,"["),ft("brackets")}for(;p.parens>0;){if(s.strictBrackets===!0)throw new SyntaxError(At("closing",")"));p.output=et.escapeLast(p.output,"("),ft("parens")}for(;p.braces>0;){if(s.strictBrackets===!0)throw new SyntaxError(At("closing","}"));p.output=et.escapeLast(p.output,"{"),ft("braces")}if(s.strictSlashes!==!0&&(c.type==="star"||c.type==="bracket")&&S({type:"maybe_slash",value:"",output:`${_}?`}),p.backtrack===!0){p.output="";for(let y of p.tokens)p.output+=y.output!=null?y.output:y.value,y.suffix&&(p.output+=y.suffix)}return p};xs.fastpaths=(e,t)=>{let s=E({},t),r=typeof s.maxLength=="number"?Math.min(zt,s.maxLength):zt,i=e.length;if(i>r)throw new SyntaxError(`Input length: ${i}, exceeds maximum allowed length: ${r}`);e=bs[e]||e;let n=et.isWindows(t),{DOT_LITERAL:o,SLASH_LITERAL:a,ONE_CHAR:d,DOTS_SLASH:f,NO_DOT:h,NO_DOTS:g,NO_DOTS_SLASH:l,STAR:_,START_ANCHOR:A}=Ut.globChars(n),R=s.dot?g:h,x=s.dot?l:h,$=s.capture?"":"?:",H={negated:!1,prefix:""},I=s.bash===!0?".*?":_;s.capture&&(I=`(${I})`);let Z=b=>b.noglobstar===!0?I:`(${$}(?:(?!${A}${b.dot?f:o}).)*?)`,j=b=>{switch(b){case"*":return`${R}${d}${I}`;case".*":return`${o}${d}${I}`;case"*.*":return`${R}${I}${o}${d}${I}`;case"*/*":return`${R}${I}${a}${d}${x}${I}`;case"**":return R+Z(s);case"**/*":return`(?:${R}${Z(s)}${a})?${x}${d}${I}`;case"**/*.*":return`(?:${R}${Z(s)}${a})?${x}${I}${o}${d}${I}`;case"**/.*":return`(?:${R}${Z(s)}${a})?${o}${d}${I}`;default:{let T=/^(.*?)\.(\w+)$/.exec(b);if(!T)return;let G=j(T[1]);return G?G+o+T[2]:void 0}}},it=et.removePrefix(e,H),F=j(it);return F&&s.strictSlashes!==!0&&(F+=`${a}?`),F};Cs.exports=xs});var $s=M((dn,Ss)=>{"use strict";var Hi=q("path"),Li=_s(),le=Es(),ce=Ct(),Oi=xt(),ki=e=>e&&typeof e=="object"&&!Array.isArray(e),z=(e,t,s=!1)=>{if(Array.isArray(e)){let h=e.map(l=>z(l,t,s));return l=>{for(let _ of h){let A=_(l);if(A)return A}return!1}}let r=ki(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!r)throw new TypeError("Expected pattern to be a non-empty string");let i=t||{},n=ce.isWindows(t),o=r?z.compileRe(e,t):z.makeRe(e,t,!1,!0),a=o.state;delete o.state;let d=()=>!1;if(i.ignore){let h=k(E({},t),{ignore:null,onMatch:null,onResult:null});d=z(i.ignore,h,s)}let f=(h,g=!1)=>{let{isMatch:l,match:_,output:A}=z.test(h,o,t,{glob:e,posix:n}),R={glob:e,state:a,regex:o,posix:n,input:h,output:A,match:_,isMatch:l};return typeof i.onResult=="function"&&i.onResult(R),l===!1?(R.isMatch=!1,g?R:!1):d(h)?(typeof i.onIgnore=="function"&&i.onIgnore(R),R.isMatch=!1,g?R:!1):(typeof i.onMatch=="function"&&i.onMatch(R),g?R:!0)};return s&&(f.state=a),f};z.test=(e,t,s,{glob:r,posix:i}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let n=s||{},o=n.format||(i?ce.toPosixSlashes:null),a=e===r,d=a&&o?o(e):e;return a===!1&&(d=o?o(e):e,a=d===r),(a===!1||n.capture===!0)&&(n.matchBase===!0||n.basename===!0?a=z.matchBase(e,t,s,i):a=t.exec(d)),{isMatch:Boolean(a),match:a,output:d}};z.matchBase=(e,t,s,r=ce.isWindows(s))=>(t instanceof RegExp?t:z.makeRe(t,s)).test(Hi.basename(e));z.isMatch=(e,t,s)=>z(t,s)(e);z.parse=(e,t)=>Array.isArray(e)?e.map(s=>z.parse(s,t)):le(e,k(E({},t),{fastpaths:!1}));z.scan=(e,t)=>Li(e,t);z.compileRe=(e,t,s=!1,r=!1)=>{if(s===!0)return e.output;let i=t||{},n=i.contains?"":"^",o=i.contains?"":"$",a=`${n}(?:${e.output})${o}`;e&&e.negated===!0&&(a=`^(?!${a}).*$`);let d=z.toRegex(a,t);return r===!0&&(d.state=e),d};z.makeRe=(e,t={},s=!1,r=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let i={negated:!1,fastpaths:!0};return t.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(i.output=le.fastpaths(e,t)),i.output||(i=le(e,t)),z.compileRe(i,t,s,r)};z.toRegex=(e,t)=>{try{let s=t||{};return new RegExp(e,s.flags||(s.nocase?"i":""))}catch(s){if(t&&t.debug===!0)throw s;return/$^/}};z.constants=Oi;Ss.exports=z});var vs=M((gn,ws)=>{"use strict";ws.exports=$s()});var ks=M((mn,Os)=>{"use strict";var Ts=q("util"),Hs=as(),lt=vs(),ue=Ct(),Ls=e=>e===""||e==="./",D=(e,t,s)=>{t=[].concat(t),e=[].concat(e);let r=new Set,i=new Set,n=new Set,o=0,a=h=>{n.add(h.output),s&&s.onResult&&s.onResult(h)};for(let h=0;h!r.has(h));if(s&&f.length===0){if(s.failglob===!0)throw new Error(`No matches found for "${t.join(", ")}"`);if(s.nonull===!0||s.nullglob===!0)return s.unescape?t.map(h=>h.replace(/\\/g,"")):t}return f};D.match=D;D.matcher=(e,t)=>lt(e,t);D.isMatch=(e,t,s)=>lt(t,s)(e);D.any=D.isMatch;D.not=(e,t,s={})=>{t=[].concat(t).map(String);let r=new Set,i=[],n=a=>{s.onResult&&s.onResult(a),i.push(a.output)},o=D(e,t,k(E({},s),{onResult:n}));for(let a of i)o.includes(a)||r.add(a);return[...r]};D.contains=(e,t,s)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${Ts.inspect(e)}"`);if(Array.isArray(t))return t.some(r=>D.contains(e,r,s));if(typeof t=="string"){if(Ls(e)||Ls(t))return!1;if(e.includes(t)||e.startsWith("./")&&e.slice(2).includes(t))return!0}return D.isMatch(e,t,k(E({},s),{contains:!0}))};D.matchKeys=(e,t,s)=>{if(!ue.isObject(e))throw new TypeError("Expected the first argument to be an object");let r=D(Object.keys(e),t,s),i={};for(let n of r)i[n]=e[n];return i};D.some=(e,t,s)=>{let r=[].concat(e);for(let i of[].concat(t)){let n=lt(String(i),s);if(r.some(o=>n(o)))return!0}return!1};D.every=(e,t,s)=>{let r=[].concat(e);for(let i of[].concat(t)){let n=lt(String(i),s);if(!r.every(o=>n(o)))return!1}return!0};D.all=(e,t,s)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${Ts.inspect(e)}"`);return[].concat(t).every(r=>lt(r,s)(e))};D.capture=(e,t,s)=>{let r=ue.isWindows(s),n=lt.makeRe(String(e),k(E({},s),{capture:!0})).exec(r?ue.toPosixSlashes(t):t);if(n)return n.slice(1).map(o=>o===void 0?"":o)};D.makeRe=(...e)=>lt.makeRe(...e);D.scan=(...e)=>lt.scan(...e);D.parse=(e,t)=>{let s=[];for(let r of[].concat(e||[]))for(let i of Hs(String(r),t))s.push(lt.parse(i,t));return s};D.braces=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return t&&t.nobrace===!0||!/\{.*\}/.test(e)?[e]:Hs(e,t)};D.braceExpand=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return D.braces(e,k(E({},t),{expand:!0}))};Os.exports=D});var he=M((yn,Ns)=>{"use strict";var v=(...e)=>e.every(t=>t)?e.join(""):"",B=e=>e?encodeURIComponent(e):"",St={sshtemplate:({domain:e,user:t,project:s,committish:r})=>`git@${e}:${t}/${s}.git${v("#",r)}`,sshurltemplate:({domain:e,user:t,project:s,committish:r})=>`git+ssh://git@${e}/${t}/${s}.git${v("#",r)}`,edittemplate:({domain:e,user:t,project:s,committish:r,editpath:i,path:n})=>`https://${e}/${t}/${s}${v("/",i,"/",B(r||"master"),"/",n)}`,browsetemplate:({domain:e,user:t,project:s,committish:r,treepath:i})=>`https://${e}/${t}/${s}${v("/",i,"/",B(r))}`,browsefiletemplate:({domain:e,user:t,project:s,committish:r,treepath:i,path:n,fragment:o,hashformat:a})=>`https://${e}/${t}/${s}/${i}/${B(r||"master")}/${n}${v("#",a(o||""))}`,docstemplate:({domain:e,user:t,project:s,treepath:r,committish:i})=>`https://${e}/${t}/${s}${v("/",r,"/",B(i))}#readme`,httpstemplate:({auth:e,domain:t,user:s,project:r,committish:i})=>`git+https://${v(e,"@")}${t}/${s}/${r}.git${v("#",i)}`,filetemplate:({domain:e,user:t,project:s,committish:r,path:i})=>`https://${e}/${t}/${s}/raw/${B(r)||"master"}/${i}`,shortcuttemplate:({type:e,user:t,project:s,committish:r})=>`${e}:${t}/${s}${v("#",r)}`,pathtemplate:({user:e,project:t,committish:s})=>`${e}/${t}${v("#",s)}`,bugstemplate:({domain:e,user:t,project:s})=>`https://${e}/${t}/${s}/issues`,hashformat:Is},rt={};rt.github=Object.assign({},St,{protocols:["git:","http:","git+ssh:","git+https:","ssh:","https:"],domain:"github.com",treepath:"tree",editpath:"edit",filetemplate:({auth:e,user:t,project:s,committish:r,path:i})=>`https://${v(e,"@")}raw.githubusercontent.com/${t}/${s}/${B(r)||"master"}/${i}`,gittemplate:({auth:e,domain:t,user:s,project:r,committish:i})=>`git://${v(e,"@")}${t}/${s}/${r}.git${v("#",i)}`,tarballtemplate:({domain:e,user:t,project:s,committish:r})=>`https://codeload.${e}/${t}/${s}/tar.gz/${B(r)||"master"}`,extract:e=>{let[,t,s,r,i]=e.pathname.split("/",5);if(!(r&&r!=="tree")&&(r||(i=e.hash.slice(1)),s&&s.endsWith(".git")&&(s=s.slice(0,-4)),!(!t||!s)))return{user:t,project:s,committish:i}}});rt.bitbucket=Object.assign({},St,{protocols:["git+ssh:","git+https:","ssh:","https:"],domain:"bitbucket.org",treepath:"src",editpath:"?mode=edit",edittemplate:({domain:e,user:t,project:s,committish:r,treepath:i,path:n,editpath:o})=>`https://${e}/${t}/${s}${v("/",i,"/",B(r||"master"),"/",n,o)}`,tarballtemplate:({domain:e,user:t,project:s,committish:r})=>`https://${e}/${t}/${s}/get/${B(r)||"master"}.tar.gz`,extract:e=>{let[,t,s,r]=e.pathname.split("/",4);if(!["get"].includes(r)&&(s&&s.endsWith(".git")&&(s=s.slice(0,-4)),!(!t||!s)))return{user:t,project:s,committish:e.hash.slice(1)}}});rt.gitlab=Object.assign({},St,{protocols:["git+ssh:","git+https:","ssh:","https:"],domain:"gitlab.com",treepath:"tree",editpath:"-/edit",httpstemplate:({auth:e,domain:t,user:s,project:r,committish:i})=>`git+https://${v(e,"@")}${t}/${s}/${r}.git${v("#",i)}`,tarballtemplate:({domain:e,user:t,project:s,committish:r})=>`https://${e}/${t}/${s}/repository/archive.tar.gz?ref=${B(r)||"master"}`,extract:e=>{let t=e.pathname.slice(1);if(t.includes("/-/")||t.includes("/archive.tar.gz"))return;let s=t.split("/"),r=s.pop();r.endsWith(".git")&&(r=r.slice(0,-4));let i=s.join("/");if(!(!i||!r))return{user:i,project:r,committish:e.hash.slice(1)}}});rt.gist=Object.assign({},St,{protocols:["git:","git+ssh:","git+https:","ssh:","https:"],domain:"gist.github.com",editpath:"edit",sshtemplate:({domain:e,project:t,committish:s})=>`git@${e}:${t}.git${v("#",s)}`,sshurltemplate:({domain:e,project:t,committish:s})=>`git+ssh://git@${e}/${t}.git${v("#",s)}`,edittemplate:({domain:e,user:t,project:s,committish:r,editpath:i})=>`https://${e}/${t}/${s}${v("/",B(r))}/${i}`,browsetemplate:({domain:e,project:t,committish:s})=>`https://${e}/${t}${v("/",B(s))}`,browsefiletemplate:({domain:e,project:t,committish:s,path:r,hashformat:i})=>`https://${e}/${t}${v("/",B(s))}${v("#",i(r))}`,docstemplate:({domain:e,project:t,committish:s})=>`https://${e}/${t}${v("/",B(s))}`,httpstemplate:({domain:e,project:t,committish:s})=>`git+https://${e}/${t}.git${v("#",s)}`,filetemplate:({user:e,project:t,committish:s,path:r})=>`https://gist.githubusercontent.com/${e}/${t}/raw${v("/",B(s))}/${r}`,shortcuttemplate:({type:e,project:t,committish:s})=>`${e}:${t}${v("#",s)}`,pathtemplate:({project:e,committish:t})=>`${e}${v("#",t)}`,bugstemplate:({domain:e,project:t})=>`https://${e}/${t}`,gittemplate:({domain:e,project:t,committish:s})=>`git://${e}/${t}.git${v("#",s)}`,tarballtemplate:({project:e,committish:t})=>`https://codeload.github.com/gist/${e}/tar.gz/${B(t)||"master"}`,extract:e=>{let[,t,s,r]=e.pathname.split("/",4);if(r!=="raw"){if(!s){if(!t)return;s=t,t=null}return s.endsWith(".git")&&(s=s.slice(0,-4)),{user:t,project:s,committish:e.hash.slice(1)}}},hashformat:function(e){return e&&"file-"+Is(e)}});rt.sourcehut=Object.assign({},St,{protocols:["git+ssh:","https:"],domain:"git.sr.ht",treepath:"tree",browsefiletemplate:({domain:e,user:t,project:s,committish:r,treepath:i,path:n,fragment:o,hashformat:a})=>`https://${e}/${t}/${s}/${i}/${B(r||"main")}/${n}${v("#",a(o||""))}`,filetemplate:({domain:e,user:t,project:s,committish:r,path:i})=>`https://${e}/${t}/${s}/blob/${B(r)||"main"}/${i}`,httpstemplate:({domain:e,user:t,project:s,committish:r})=>`https://${e}/${t}/${s}.git${v("#",r)}`,tarballtemplate:({domain:e,user:t,project:s,committish:r})=>`https://${e}/${t}/${s}/archive/${B(r)||"main"}.tar.gz`,bugstemplate:({domain:e,user:t,project:s})=>`https://todo.sr.ht/${t}/${s}`,docstemplate:({domain:e,user:t,project:s,treepath:r,committish:i})=>`https://${e}/${t}/${s}${v("/",r,"/",B(i))}#readme`,extract:e=>{let[,t,s,r]=e.pathname.split("/",4);if(!["archive"].includes(r)&&(s&&s.endsWith(".git")&&(s=s.slice(0,-4)),!(!t||!s)))return{user:t,project:s,committish:e.hash.slice(1)}}});var Ii=Object.keys(rt);rt.byShortcut={};rt.byDomain={};for(let e of Ii)rt.byShortcut[`${e}:`]=e,rt.byDomain[rt[e].domain]=e;function Is(e){return e.toLowerCase().replace(/^\W+|\/|\W+$/g,"").replace(/\W+/g,"-")}Ns.exports=rt});var Ps=M((An,Ms)=>{"use strict";var Ni=he(),Ds=class{constructor(t,s,r,i,n,o,a={}){Object.assign(this,Ni[t]),this.type=t,this.user=s,this.auth=r,this.project=i,this.committish=n,this.default=o,this.opts=a}hash(){return this.committish?`#${this.committish}`:""}ssh(t){return this._fill(this.sshtemplate,t)}_fill(t,s){if(typeof t=="function"){let r=E(E(E({},this),this.opts),s);r.path||(r.path=""),r.path.startsWith("/")&&(r.path=r.path.slice(1)),r.noCommittish&&(r.committish=null);let i=t(r);return r.noGitPlus&&i.startsWith("git+")?i.slice(4):i}return null}sshurl(t){return this._fill(this.sshurltemplate,t)}browse(t,s,r){return typeof t!="string"?this._fill(this.browsetemplate,t):(typeof s!="string"&&(r=s,s=null),this._fill(this.browsefiletemplate,k(E({},r),{fragment:s,path:t})))}docs(t){return this._fill(this.docstemplate,t)}bugs(t){return this._fill(this.bugstemplate,t)}https(t){return this._fill(this.httpstemplate,t)}git(t){return this._fill(this.gittemplate,t)}shortcut(t){return this._fill(this.shortcuttemplate,t)}path(t){return this._fill(this.pathtemplate,t)}tarball(t){return this._fill(this.tarballtemplate,k(E({},t),{noCommittish:!1}))}file(t,s){return this._fill(this.filetemplate,k(E({},s),{path:t}))}edit(t,s){return this._fill(this.edittemplate,k(E({},s),{path:t}))}getDefaultRepresentation(){return this.default}toString(t){return this.default&&typeof this[this.default]=="function"?this[this.default](t):this.sshurl(t)}};Ms.exports=Ds});var Ws=M((bn,Fs)=>{var $t=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date,Di=typeof AbortController=="function",Bt=Di?AbortController:class{constructor(){this.signal=new Us}abort(){this.signal.dispatchEvent("abort")}},Mi=typeof AbortSignal=="function",Pi=typeof Bt.AbortSignal=="function",Us=Mi?AbortSignal:Pi?Bt.AbortController:class{constructor(){this.aborted=!1,this._listeners=[]}dispatchEvent(t){if(t==="abort"){this.aborted=!0;let s={type:t,target:this};this.onabort(s),this._listeners.forEach(r=>r(s),this)}}onabort(){}addEventListener(t,s){t==="abort"&&this._listeners.push(s)}removeEventListener(t,s){t==="abort"&&(this._listeners=this._listeners.filter(r=>r!==s))}},pe=new Set,fe=(e,t)=>{let s=`LRU_CACHE_OPTION_${e}`;jt(s)&&ge(s,`${e} option`,`options.${t}`,pt)},de=(e,t)=>{let s=`LRU_CACHE_METHOD_${e}`;if(jt(s)){let{prototype:r}=pt,{get:i}=Object.getOwnPropertyDescriptor(r,e);ge(s,`${e} method`,`cache.${t}()`,i)}},Ui=(e,t)=>{let s=`LRU_CACHE_PROPERTY_${e}`;if(jt(s)){let{prototype:r}=pt,{get:i}=Object.getOwnPropertyDescriptor(r,e);ge(s,`${e} property`,`cache.${t}`,i)}},zs=(...e)=>{typeof process=="object"&&process&&typeof process.emitWarning=="function"?process.emitWarning(...e):console.error(...e)},jt=e=>!pe.has(e),ge=(e,t,s,r)=>{pe.add(e);let i=`The ${t} is deprecated. Please use ${s} instead.`;zs(i,"DeprecationWarning",e,r)},ht=e=>e&&e===Math.floor(e)&&e>0&&isFinite(e),Bs=e=>ht(e)?e<=Math.pow(2,8)?Uint8Array:e<=Math.pow(2,16)?Uint16Array:e<=Math.pow(2,32)?Uint32Array:e<=Number.MAX_SAFE_INTEGER?wt:null:null,wt=class extends Array{constructor(t){super(t);this.fill(0)}},js=class{constructor(t){if(t===0)return[];let s=Bs(t);this.heap=new s(t),this.length=0}push(t){this.heap[this.length++]=t}pop(){return this.heap[--this.length]}},pt=class{constructor(t={}){let{max:s=0,ttl:r,ttlResolution:i=1,ttlAutopurge:n,updateAgeOnGet:o,updateAgeOnHas:a,allowStale:d,dispose:f,disposeAfter:h,noDisposeOnSet:g,noUpdateTTL:l,maxSize:_=0,maxEntrySize:A=0,sizeCalculation:R,fetchMethod:x,fetchContext:$,noDeleteOnFetchRejection:H,noDeleteOnStaleGet:I}=t,{length:Z,maxAge:j,stale:it}=t instanceof pt?{}:t;if(s!==0&&!ht(s))throw new TypeError("max option must be a nonnegative integer");let F=s?Bs(s):Array;if(!F)throw new Error("invalid max value: "+s);if(this.max=s,this.maxSize=_,this.maxEntrySize=A||this.maxSize,this.sizeCalculation=R||Z,this.sizeCalculation){if(!this.maxSize&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(this.fetchMethod=x||null,this.fetchMethod&&typeof this.fetchMethod!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.fetchContext=$,!this.fetchMethod&&$!==void 0)throw new TypeError("cannot set fetchContext without fetchMethod");if(this.keyMap=new Map,this.keyList=new Array(s).fill(null),this.valList=new Array(s).fill(null),this.next=new F(s),this.prev=new F(s),this.head=0,this.tail=0,this.free=new js(s),this.initialFill=1,this.size=0,typeof f=="function"&&(this.dispose=f),typeof h=="function"?(this.disposeAfter=h,this.disposed=[]):(this.disposeAfter=null,this.disposed=null),this.noDisposeOnSet=!!g,this.noUpdateTTL=!!l,this.noDeleteOnFetchRejection=!!H,this.maxEntrySize!==0){if(this.maxSize!==0&&!ht(this.maxSize))throw new TypeError("maxSize must be a positive integer if specified");if(!ht(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.initializeSizeTracking()}if(this.allowStale=!!d||!!it,this.noDeleteOnStaleGet=!!I,this.updateAgeOnGet=!!o,this.updateAgeOnHas=!!a,this.ttlResolution=ht(i)||i===0?i:1,this.ttlAutopurge=!!n,this.ttl=r||j||0,this.ttl){if(!ht(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.initializeTTLTracking()}if(this.max===0&&this.ttl===0&&this.maxSize===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.max&&!this.maxSize){let b="LRU_CACHE_UNBOUNDED";jt(b)&&(pe.add(b),zs("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",b,pt))}it&&fe("stale","allowStale"),j&&fe("maxAge","ttl"),Z&&fe("length","sizeCalculation")}getRemainingTTL(t){return this.has(t,{updateAgeOnHas:!1})?Infinity:0}initializeTTLTracking(){this.ttls=new wt(this.max),this.starts=new wt(this.max),this.setItemTTL=(r,i,n=$t.now())=>{if(this.starts[r]=i!==0?n:0,this.ttls[r]=i,i!==0&&this.ttlAutopurge){let o=setTimeout(()=>{this.isStale(r)&&this.delete(this.keyList[r])},i+1);o.unref&&o.unref()}},this.updateItemAge=r=>{this.starts[r]=this.ttls[r]!==0?$t.now():0};let t=0,s=()=>{let r=$t.now();if(this.ttlResolution>0){t=r;let i=setTimeout(()=>t=0,this.ttlResolution);i.unref&&i.unref()}return r};this.getRemainingTTL=r=>{let i=this.keyMap.get(r);return i===void 0?0:this.ttls[i]===0||this.starts[i]===0?Infinity:this.starts[i]+this.ttls[i]-(t||s())},this.isStale=r=>this.ttls[r]!==0&&this.starts[r]!==0&&(t||s())-this.starts[r]>this.ttls[r]}updateItemAge(t){}setItemTTL(t,s,r){}isStale(t){return!1}initializeSizeTracking(){this.calculatedSize=0,this.sizes=new wt(this.max),this.removeItemSize=t=>{this.calculatedSize-=this.sizes[t],this.sizes[t]=0},this.requireSize=(t,s,r,i)=>{if(!ht(r))if(i){if(typeof i!="function")throw new TypeError("sizeCalculation must be a function");if(r=i(s,t),!ht(r))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer)");return r},this.addItemSize=(t,s)=>{this.sizes[t]=s;let r=this.maxSize-this.sizes[t];for(;this.calculatedSize>r;)this.evict(!0);this.calculatedSize+=this.sizes[t]}}removeItemSize(t){}addItemSize(t,s){}requireSize(t,s,r,i){if(r||i)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache")}*indexes({allowStale:t=this.allowStale}={}){if(this.size)for(let s=this.tail;!(!this.isValidIndex(s)||((t||!this.isStale(s))&&(yield s),s===this.head));)s=this.prev[s]}*rindexes({allowStale:t=this.allowStale}={}){if(this.size)for(let s=this.head;!(!this.isValidIndex(s)||((t||!this.isStale(s))&&(yield s),s===this.tail));)s=this.next[s]}isValidIndex(t){return this.keyMap.get(this.keyList[t])===t}*entries(){for(let t of this.indexes())yield[this.keyList[t],this.valList[t]]}*rentries(){for(let t of this.rindexes())yield[this.keyList[t],this.valList[t]]}*keys(){for(let t of this.indexes())yield this.keyList[t]}*rkeys(){for(let t of this.rindexes())yield this.keyList[t]}*values(){for(let t of this.indexes())yield this.valList[t]}*rvalues(){for(let t of this.rindexes())yield this.valList[t]}[Symbol.iterator](){return this.entries()}find(t,s={}){for(let r of this.indexes())if(t(this.valList[r],this.keyList[r],this))return this.get(this.keyList[r],s)}forEach(t,s=this){for(let r of this.indexes())t.call(s,this.valList[r],this.keyList[r],this)}rforEach(t,s=this){for(let r of this.rindexes())t.call(s,this.valList[r],this.keyList[r],this)}get prune(){return de("prune","purgeStale"),this.purgeStale}purgeStale(){let t=!1;for(let s of this.rindexes({allowStale:!0}))this.isStale(s)&&(this.delete(this.keyList[s]),t=!0);return t}dump(){let t=[];for(let s of this.indexes({allowStale:!0})){let r=this.keyList[s],i=this.valList[s],o={value:this.isBackgroundFetch(i)?i.__staleWhileFetching:i};if(this.ttls){o.ttl=this.ttls[s];let a=$t.now()-this.starts[s];o.start=Math.floor(Date.now()-a)}this.sizes&&(o.size=this.sizes[s]),t.unshift([r,o])}return t}load(t){this.clear();for(let[s,r]of t){if(r.start){let i=Date.now()-r.start;r.start=$t.now()-i}this.set(s,r.value,r)}}dispose(t,s,r){}set(t,s,{ttl:r=this.ttl,start:i,noDisposeOnSet:n=this.noDisposeOnSet,size:o=0,sizeCalculation:a=this.sizeCalculation,noUpdateTTL:d=this.noUpdateTTL}={}){if(o=this.requireSize(t,s,o,a),this.maxEntrySize&&o>this.maxEntrySize)return this;let f=this.size===0?void 0:this.keyMap.get(t);if(f===void 0)f=this.newIndex(),this.keyList[f]=t,this.valList[f]=s,this.keyMap.set(t,f),this.next[this.tail]=f,this.prev[f]=this.tail,this.tail=f,this.size++,this.addItemSize(f,o),d=!1;else{let h=this.valList[f];s!==h&&(this.isBackgroundFetch(h)?h.__abortController.abort():n||(this.dispose(h,t,"set"),this.disposeAfter&&this.disposed.push([h,t,"set"])),this.removeItemSize(f),this.valList[f]=s,this.addItemSize(f,o)),this.moveToTail(f)}if(r!==0&&this.ttl===0&&!this.ttls&&this.initializeTTLTracking(),d||this.setItemTTL(f,r,i),this.disposeAfter)for(;this.disposed.length;)this.disposeAfter(...this.disposed.shift());return this}newIndex(){return this.size===0?this.tail:this.size===this.max&&this.max!==0?this.evict(!1):this.free.length!==0?this.free.pop():this.initialFill++}pop(){if(this.size){let t=this.valList[this.head];return this.evict(!0),t}}evict(t){let s=this.head,r=this.keyList[s],i=this.valList[s];return this.isBackgroundFetch(i)?i.__abortController.abort():(this.dispose(i,r,"evict"),this.disposeAfter&&this.disposed.push([i,r,"evict"])),this.removeItemSize(s),t&&(this.keyList[s]=null,this.valList[s]=null,this.free.push(s)),this.head=this.next[s],this.keyMap.delete(r),this.size--,s}has(t,{updateAgeOnHas:s=this.updateAgeOnHas}={}){let r=this.keyMap.get(t);return r!==void 0&&!this.isStale(r)?(s&&this.updateItemAge(r),!0):!1}peek(t,{allowStale:s=this.allowStale}={}){let r=this.keyMap.get(t);if(r!==void 0&&(s||!this.isStale(r))){let i=this.valList[r];return this.isBackgroundFetch(i)?i.__staleWhileFetching:i}}backgroundFetch(t,s,r,i){let n=s===void 0?void 0:this.valList[s];if(this.isBackgroundFetch(n))return n;let o=new Bt,a={signal:o.signal,options:r,context:i},d=l=>(o.signal.aborted||this.set(t,l,a.options),l),f=l=>{if(this.valList[s]===g&&(!r.noDeleteOnFetchRejection||g.__staleWhileFetching===void 0?this.delete(t):this.valList[s]=g.__staleWhileFetching),g.__returned===g)throw l},h=l=>l(this.fetchMethod(t,n,a)),g=new Promise(h).then(d,f);return g.__abortController=o,g.__staleWhileFetching=n,g.__returned=null,s===void 0?(this.set(t,g,a.options),s=this.keyMap.get(t)):this.valList[s]=g,g}isBackgroundFetch(t){return t&&typeof t=="object"&&typeof t.then=="function"&&Object.prototype.hasOwnProperty.call(t,"__staleWhileFetching")&&Object.prototype.hasOwnProperty.call(t,"__returned")&&(t.__returned===t||t.__returned===null)}async fetch(t,{allowStale:s=this.allowStale,updateAgeOnGet:r=this.updateAgeOnGet,noDeleteOnStaleGet:i=this.noDeleteOnStaleGet,ttl:n=this.ttl,noDisposeOnSet:o=this.noDisposeOnSet,size:a=0,sizeCalculation:d=this.sizeCalculation,noUpdateTTL:f=this.noUpdateTTL,noDeleteOnFetchRejection:h=this.noDeleteOnFetchRejection,fetchContext:g=this.fetchContext,forceRefresh:l=!1}={}){if(!this.fetchMethod)return this.get(t,{allowStale:s,updateAgeOnGet:r,noDeleteOnStaleGet:i});let _={allowStale:s,updateAgeOnGet:r,noDeleteOnStaleGet:i,ttl:n,noDisposeOnSet:o,size:a,sizeCalculation:d,noUpdateTTL:f,noDeleteOnFetchRejection:h},A=this.keyMap.get(t);if(A===void 0){let R=this.backgroundFetch(t,A,_,g);return R.__returned=R}else{let R=this.valList[A];if(this.isBackgroundFetch(R))return s&&R.__staleWhileFetching!==void 0?R.__staleWhileFetching:R.__returned=R;if(!l&&!this.isStale(A))return this.moveToTail(A),r&&this.updateItemAge(A),R;let x=this.backgroundFetch(t,A,_,g);return s&&x.__staleWhileFetching!==void 0?x.__staleWhileFetching:x.__returned=x}}get(t,{allowStale:s=this.allowStale,updateAgeOnGet:r=this.updateAgeOnGet,noDeleteOnStaleGet:i=this.noDeleteOnStaleGet}={}){let n=this.keyMap.get(t);if(n!==void 0){let o=this.valList[n],a=this.isBackgroundFetch(o);return this.isStale(n)?a?s?o.__staleWhileFetching:void 0:(i||this.delete(t),s?o:void 0):a?void 0:(this.moveToTail(n),r&&this.updateItemAge(n),o)}}connect(t,s){this.prev[s]=t,this.next[t]=s}moveToTail(t){t!==this.tail&&(t===this.head?this.head=this.next[t]:this.connect(this.prev[t],this.next[t]),this.connect(this.tail,t),this.tail=t)}get del(){return de("del","delete"),this.delete}delete(t){let s=!1;if(this.size!==0){let r=this.keyMap.get(t);if(r!==void 0)if(s=!0,this.size===1)this.clear();else{this.removeItemSize(r);let i=this.valList[r];this.isBackgroundFetch(i)?i.__abortController.abort():(this.dispose(i,t,"delete"),this.disposeAfter&&this.disposed.push([i,t,"delete"])),this.keyMap.delete(t),this.keyList[r]=null,this.valList[r]=null,r===this.tail?this.tail=this.prev[r]:r===this.head?this.head=this.next[r]:(this.next[this.prev[r]]=this.next[r],this.prev[this.next[r]]=this.prev[r]),this.size--,this.free.push(r)}}if(this.disposed)for(;this.disposed.length;)this.disposeAfter(...this.disposed.shift());return s}clear(){for(let t of this.rindexes({allowStale:!0})){let s=this.valList[t];if(this.isBackgroundFetch(s))s.__abortController.abort();else{let r=this.keyList[t];this.dispose(s,r,"delete"),this.disposeAfter&&this.disposed.push([s,r,"delete"])}}if(this.keyMap.clear(),this.valList.fill(null),this.keyList.fill(null),this.ttls&&(this.ttls.fill(0),this.starts.fill(0)),this.sizes&&this.sizes.fill(0),this.head=0,this.tail=0,this.initialFill=1,this.free.length=0,this.calculatedSize=0,this.size=0,this.disposed)for(;this.disposed.length;)this.disposeAfter(...this.disposed.shift())}get reset(){return de("reset","clear"),this.clear}get length(){return Ui("length","size"),this.size}static get AbortController(){return Bt}static get AbortSignal(){return Us}};Fs.exports=pt});var me=M((xn,Gs)=>{Gs.exports=(e={})=>E({"git+ssh:":{name:"sshurl"},"ssh:":{name:"sshurl"},"git+https:":{name:"https",auth:!0},"git:":{auth:!0},"http:":{auth:!0},"https:":{auth:!0},"git+http:":{auth:!0}},Object.keys(e).reduce((t,s)=>(t[s]={name:e[s]},t),{}))});var Qs=M((Cn,Ks)=>{var zi=q("url"),Bi=me(),ye=(e,t,s)=>{let r=e.indexOf(s);return e.lastIndexOf(t,r>-1?r:Infinity)},qs=e=>{try{return new zi.URL(e)}catch{}},ji=(e,t)=>{let s=e.indexOf(":"),r=e.slice(0,s+1);if(Object.prototype.hasOwnProperty.call(t,r))return e;let i=e.indexOf("@");return i>-1?i>s?`git+ssh://${e}`:e:e.indexOf("//")===s+1?e:`${e.slice(0,s+1)}//${e.slice(s+1)}`},Fi=e=>{let t=ye(e,"@","#"),s=ye(e,":","#");return s>t&&(e=e.slice(0,s)+"/"+e.slice(s+1)),ye(e,":","#")===-1&&e.indexOf("//")===-1&&(e=`git+ssh://${e}`),e};Ks.exports=(e,t=Bi())=>{let s=ji(e,t);return qs(s)||qs(Fi(s))}});var Vs=M((En,Wt)=>{"use strict";var Ft=he(),Wi=Wt.exports=Ps(),Gi=Ws(),Xs=Qs(),vt=me()(Ft.byShortcut),Ae=new Gi({max:1e3});Wt.exports.fromUrl=function(e,t){if(typeof e!="string")return;let s=e+JSON.stringify(t||{});return Ae.has(s)||Ae.set(s,qi(e,t)),Ae.get(s)};Wt.exports.parseUrl=Xs;function qi(e,t){if(!e)return;let s=Ki(e)?`github:${e}`:e,r=Xs(s,vt);if(!r)return;let i=Ft.byShortcut[r.protocol],n=Ft.byDomain[r.hostname.startsWith("www.")?r.hostname.slice(4):r.hostname],o=i||n;if(!o)return;let a=Ft[i||n],d=null;vt[r.protocol]&&vt[r.protocol].auth&&(r.username||r.password)&&(d=`${r.username}${r.password?":"+r.password:""}`);let f=null,h=null,g=null,l=null;try{if(i){let _=r.pathname.startsWith("/")?r.pathname.slice(1):r.pathname,A=_.indexOf("@");A>-1&&(_=_.slice(A+1));let R=_.lastIndexOf("/");R>-1?(h=decodeURIComponent(_.slice(0,R)),h||(h=null),g=decodeURIComponent(_.slice(R+1))):g=decodeURIComponent(_),g.endsWith(".git")&&(g=g.slice(0,-4)),r.hash&&(f=decodeURIComponent(r.hash.slice(1))),l="shortcut"}else{if(!a.protocols.includes(r.protocol))return;let _=a.extract(r);if(!_)return;h=_.user&&decodeURIComponent(_.user),g=decodeURIComponent(_.project),f=decodeURIComponent(_.committish),l=vt[r.protocol]&&vt[r.protocol].name||r.protocol.slice(0,-1)}}catch(_){if(_ instanceof URIError)return;throw _}return new Wi(o,h,d,g,f,l,t)}var Ki=e=>{let t=e.indexOf("#"),s=e.indexOf("/"),r=e.indexOf("/",s+1),i=e.indexOf(":"),n=/\s/.exec(e),o=e.indexOf("@"),a=!n||t>-1&&n.index>t,d=o===-1||t>-1&&o>t,f=i===-1||t>-1&&i>t,h=r===-1||t>-1&&r>t,g=s>0,l=t>-1?e[t-1]!=="/":!e.endsWith("/"),_=!e.startsWith(".");return a&&g&&l&&_&&d&&f&&h}});var Zi={};Ar(Zi,{default:()=>Vi});var ar=J(q("@yarnpkg/core"));var Kt=J(q("@yarnpkg/cli")),P=J(q("@yarnpkg/core")),V=J(q("clipanion")),be=J(ks()),nr=J(q("path")),Qt=J(q("semver")),mt=J(q("typanion"));var Rt=J(q("@yarnpkg/core")),er=J(q("@yarnpkg/plugin-essentials"));var Zs=J(Vs()),Tt=J(q("semver")),Ys=Boolean;function Js({raw:e}){if(e.homepage)return e.homepage;let t=e.repository,s=typeof t=="string"?t:typeof t=="object"&&typeof t.url=="string"?t.url:null,r=s?(0,Zs.fromUrl)(s):void 0,i=(r==null?void 0:r.committish)?`#${r.committish}`:"";return r?`https://${r.domain}/${r.user}/${r.project}${i}`:s}function tr(e,t){return Tt.default.parse(t).prerelease.length?Tt.default.lt(e,t):Tt.default.lt(Tt.default.coerce(e),t)}var Re=class{constructor(t,s,r,i){this.configuration=t;this.project=s;this.workspace=r;this.cache=i}async fetch({descriptor:t,includeRange:s,includeURL:r,pkg:i}){let[n,o,a]=await Promise.all([this.suggest(i,"latest"),s?this.suggest(i,t.range):Promise.resolve(),r?this.fetchURL(i):Promise.resolve()]);if(!n){let d=Rt.structUtils.prettyIdent(this.configuration,i);throw new Error(`Could not fetch candidate for ${d}.`)}return{latest:n.range,range:o==null?void 0:o.range,url:a!=null?a:void 0}}suggest(t,s){return er.suggestUtils.fetchDescriptorFrom(t,s,{cache:this.cache,preserveModifier:!1,project:this.project,workspace:this.workspace})}async fetchURL(t){var n;let s=this.configuration.makeFetcher(),r=await s.fetch(t,{cache:this.cache,checksums:this.project.storedChecksums,fetcher:s,project:this.project,report:new Rt.ThrowReport,skipIntegrityCheck:!0}),i;try{i=await Rt.Manifest.find(r.prefixPath,{baseFs:r.packageFs})}finally{(n=r.releaseFs)==null||n.call(r)}return Js(i)}};var Gt=J(q("@yarnpkg/core")),Qi=/^([0-9]+\.)([0-9]+\.)(.+)$/,sr=["name","current","range","latest","workspace","type","url"],qt=class{constructor(t,s,r,i,n){this.format=t;this.writer=s;this.configuration=r;this.dependencies=i;this.extraColumns=n;this.sizes=null;this.headers={current:"Current",latest:"Latest",name:"Package",range:"Range",type:"Package Type",url:"URL",workspace:"Workspace"}}print(){this.sizes=this.getColumnSizes(),this.printHeader(),this.dependencies.forEach(t=>{var i,n;let s=this.getDiffColor(t.severity.latest),r=this.getDiffColor(t.severity.range);this.printRow({current:t.current.padEnd(this.sizes.current),latest:this.formatVersion(t,"latest",s),name:this.applyColor(t.name.padEnd(this.sizes.name),s),range:this.formatVersion(t,"range",r),type:t.type.padEnd(this.sizes.type),url:(i=t.url)==null?void 0:i.padEnd(this.sizes.url),workspace:(n=t.workspace)==null?void 0:n.padEnd(this.sizes.workspace)})})}applyColor(t,s){return s?Gt.formatUtils.pretty(this.configuration,t,s):t}formatVersion(t,s,r){var f;let i=(f=t[s])==null?void 0:f.padEnd(this.sizes[s]);if(!i)return;let n=i.match(Qi);if(!n||!r)return i;let o=["red","yellow","green"].indexOf(r)+1,a=n.slice(1,o).join(""),d=n.slice(o).join("");return a+Gt.formatUtils.pretty(this.configuration,this.applyColor(d,r),"bold")}getDiffColor(t){return t?{major:"red",minor:"yellow",patch:"green"}[t]:null}getColumnSizes(){let t=sr.reduce((s,r)=>k(E({},s),{[r]:this.headers[r].length}),{});for(let s of this.dependencies)for(let[r,i]of Object.entries(s)){let n=t[r],o=(i||"").length;t[r]=n>o?n:o}return t}formatColumnHeader(t){return Gt.formatUtils.pretty(this.configuration,this.headers[t].padEnd(this.sizes[t]),"bold")}printHeader(){this.printRow({current:this.formatColumnHeader("current"),latest:this.formatColumnHeader("latest"),name:this.formatColumnHeader("name"),range:this.formatColumnHeader("range"),type:this.formatColumnHeader("type"),url:this.formatColumnHeader("url"),workspace:this.formatColumnHeader("workspace")}),this.format==="markdown"&&this.printRow(Object.keys(this.sizes).reduce((t,s)=>k(E({},t),{[s]:"".padEnd(this.sizes[s],"-")}),{}))}printRow(t){let s=this.format==="markdown",r=sr.filter(i=>{var n;return(n=this.extraColumns[i])!=null?n:!0}).map(i=>t[i]).join(s?" | ":" ");this.writer(s?`| ${r} |`:r.trim())}};var _e=["dependencies","devDependencies"],rr=["major","minor","patch"],ir=["text","json","markdown"];var or="\u2728 All your dependencies are up to date!",Ht=class extends Kt.BaseCommand{constructor(){super(...arguments);this.patterns=V.Option.Rest();this.workspace=V.Option.Array("-w,--workspace",{description:"Only search for dependencies in the specified workspaces. If no workspaces are specified, only searches for outdated dependencies in the current workspace.",validator:mt.default.isArray(mt.default.isString())});this.check=V.Option.Boolean("-c,--check",!1,{description:"Exit with exit code 1 when outdated dependencies are found"});this.format=V.Option.String("--format","text",{description:"The format of the output (text|json|markdown)",validator:mt.default.isEnum(ir)});this.json=V.Option.Boolean("--json",!1,{description:"Format the output as JSON"});this.severity=V.Option.Array("-s,--severity",{description:"Filter results based on the severity of the update",validator:mt.default.isArray(mt.default.isEnum(rr))});this.type=V.Option.String("-t,--type",{description:"Filter results based on the dependency type",validator:mt.default.isEnum(_e)});this._includeURL=V.Option.Boolean("--url",{description:"Include the homepage URL of each package in the output"});this.includeRange=V.Option.Boolean("--range",!1,{description:"Include the latest version of the package which satisfies the current range specified in the manifest."})}async execute(){let{cache:t,configuration:s,project:r,workspace:i}=await this.loadProject(),n=new Re(s,r,i,t),o=this.getWorkspaces(r),a=this.getDependencies(s,o);if(this.format!=="text"||this.json){let f=await this.getOutdatedDependencies(s,r,n,a);return this.format==="json"||this.json?this.writeJson(f):this.writeMarkdown(s,r,f),f.length?1:0}return(await P.StreamReport.start({configuration:s,stdout:this.context.stdout},async f=>{await this.checkOutdatedDependencies(s,r,a,n,f)})).exitCode()}includeURL(t){var s;return(s=this._includeURL)!=null?s:t.get("outdatedIncludeUrl")}writeJson(t){let s=t.map(r=>k(E({},r),{severity:r.severity.latest}));this.context.stdout.write(JSON.stringify(s)+` 8 | `)}writeMarkdown(t,s,r){if(!r.length){this.context.stdout.write(or+` 9 | `);return}new qt("markdown",n=>this.context.stdout.write(n+` 10 | `),t,r,{range:this.includeRange,url:this.includeURL(t),workspace:this.includeWorkspace(s)}).print()}async checkOutdatedDependencies(t,s,r,i,n){let o=null;await n.startTimerPromise("Checking for outdated dependencies",async()=>{let a=r.length,d=P.StreamReport.progressViaCounter(a);n.reportProgress(d),o=await this.getOutdatedDependencies(t,s,i,r,d)}),n.reportSeparator(),o.length?(new qt("text",d=>n.reportInfo(P.MessageName.UNNAMED,d),t,o,{range:this.includeRange,url:this.includeURL(t),workspace:this.includeWorkspace(s)}).print(),n.reportSeparator(),this.printOutdatedCount(n,o.length)):n.reportInfo(P.MessageName.UNNAMED,P.formatUtils.pretty(t,or,"green"))}async loadProject(){let t=await P.Configuration.find(this.context.cwd,this.context.plugins),[s,{project:r,workspace:i}]=await Promise.all([P.Cache.find(t),P.Project.find(t,this.context.cwd)]);if(await r.restoreInstallState(),!i)throw new Kt.WorkspaceRequiredError(r.cwd,this.context.cwd);return{cache:s,configuration:t,project:r,workspace:i}}getWorkspaces(t){let s=this.workspace;return s?s[0]==="."?t.workspaces.filter(r=>r.cwd===this.context.cwd):t.workspaces.filter(r=>{let i=[...s,...s.map(n=>nr.default.join(this.context.cwd,n))];return be.default.some([this.getWorkspaceName(r),r.cwd],i)}):t.workspaces}includeWorkspace(t){return t.workspaces.length>1}get dependencyTypes(){return this.type?[this.type]:_e}getDependencies(t,s){let r=[];for(let n of s){let{anchoredLocator:o,project:a}=n,d=a.storedPackages.get(o.locatorHash);d||this.throw(t,o);for(let f of this.dependencyTypes)for(let h of n.manifest[f].values()){let{range:g}=h;if(g.includes(":")&&!/(npm|patch):/.test(g))continue;let l=d.dependencies.get(h.identHash);l||this.throw(t,h);let _=a.storedResolutions.get(l.descriptorHash);_||this.throw(t,l);let A=a.storedPackages.get(_);A||this.throw(t,l),!n.project.tryWorkspaceByLocator(A)&&(A.reference.includes("github.com")||r.push({dependencyType:f,descriptor:h,name:P.structUtils.stringifyIdent(h),pkg:A,workspace:n}))}}if(!this.patterns.length)return r;let i=r.filter(({name:n})=>be.default.isMatch(n,this.patterns));if(!i.length)throw new V.UsageError(`Pattern ${P.formatUtils.prettyList(t,this.patterns,P.FormatType.CODE)} doesn't match any packages referenced by any workspace`);return i}throw(t,s){let r=P.structUtils.prettyIdent(t,s);throw new Error(`Package for ${r} not found in the project`)}getSeverity(t,s){let r=Qt.default.coerce(t),i=Qt.default.coerce(s);return Qt.default.eq(r,i)?null:r.major===0||i.major>r.major?"major":i.minor>r.minor?"minor":"patch"}async getOutdatedDependencies(t,s,r,i,n){let o=i.map(async({dependencyType:a,descriptor:d,name:f,pkg:h,workspace:g})=>{let{latest:l,range:_,url:A}=await r.fetch({descriptor:d,includeRange:this.includeRange,includeURL:this.includeURL(t),pkg:h});if(n==null||n.tick(),tr(h.version,l))return{current:h.version,latest:l,name:f,range:_,severity:{latest:this.getSeverity(h.version,l),range:_?this.getSeverity(h.version,_):null},type:a,url:A,workspace:this.includeWorkspace(s)?this.getWorkspaceName(g):void 0}});return(await Promise.all(o)).filter(Ys).filter(a=>{var d,f;return(f=(d=this.severity)==null?void 0:d.includes(a.severity.latest))!=null?f:!0}).sort((a,d)=>a.name.localeCompare(d.name))}getWorkspaceName(t){return t.manifest.name?P.structUtils.stringifyIdent(t.manifest.name):t.computeCandidateName()}printOutdatedCount(t,s){let r=[P.MessageName.UNNAMED,s===1?"1 dependency is out of date":`${s} dependencies are out of date`];this.check?t.reportError(...r):t.reportWarning(...r)}};Ht.paths=[["outdated"]],Ht.usage=V.Command.Usage({description:"view outdated dependencies",details:` 11 | This command finds outdated dependencies in a project and prints the result in a table or JSON format. 12 | 13 | This command accepts glob patterns as arguments to filter the output. Make sure to escape the patterns, to prevent your own shell from trying to expand them. 14 | `,examples:[["View outdated dependencies","yarn outdated"],["View outdated dependencies with the `@babel` scope","yarn outdated '@babel/*'"],["Filter results to only include devDependencies","yarn outdated --type devDependencies"],["Filter results to only include major version updates","yarn outdated --severity major"]]});var Xi={commands:[Ht],configuration:{outdatedIncludeUrl:{default:!1,description:"If true, the outdated command will include the package homepage URL by default",type:ar.SettingsType.BOOLEAN}}},Vi=Xi;return Zi;})(); 15 | /*! 16 | * fill-range 17 | * 18 | * Copyright (c) 2014-present, Jon Schlinkert. 19 | * Licensed under the MIT License. 20 | */ 21 | /*! 22 | * is-number 23 | * 24 | * Copyright (c) 2014-present, Jon Schlinkert. 25 | * Released under the MIT License. 26 | */ 27 | /*! 28 | * to-regex-range 29 | * 30 | * Copyright (c) 2015-present, Jon Schlinkert. 31 | * Released under the MIT License. 32 | */ 33 | return plugin; 34 | } 35 | }; 36 | --------------------------------------------------------------------------------