├── .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 |
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 |
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 | [](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 |
--------------------------------------------------------------------------------