diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 111b2198c..5c0fd659f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 955b3b3fb..7c997cfd1 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit diff --git a/.github/workflows/multibuild.yaml b/.github/workflows/multibuild.yaml index 6084c8882..edfb465b7 100644 --- a/.github/workflows/multibuild.yaml +++ b/.github/workflows/multibuild.yaml @@ -74,15 +74,9 @@ jobs: with: ref: multibuild-${{github.run_number}} - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 # v1.6.5 - - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - with: - node-version: '20.17.0' - # setup required npm version - - run: | - npm install -g npm@10.8.2 - # create directories need for build + # create directories needed for build - run: | - mkdir -p sshnp/web/admin + mkdir sshnp mkdir tarball - if: ${{ matrix.os != 'windows-latest' }} run: mkdir sshnp/debug @@ -103,11 +97,6 @@ jobs: run: | dart pub get --enforce-lockfile dart compile exe bin/np_admin.dart -v -o ../../../packages/dart/sshnoports/sshnp/np_admin${{ matrix.ext }} - - name: build admin webapp - working-directory: ./apps/admin/webapp - run: | - npm ci - npm run build - if: ${{ matrix.os != 'windows-latest' }} run: | dart compile exe bin/srvd.dart -v -o sshnp/srvd${{ matrix.ext }} @@ -116,7 +105,6 @@ jobs: - run: | cp -r bundles/core/* sshnp/ cp -r bundles/${{ matrix.bundle }}/* sshnp/ - cp -r ../../../apps/admin/webapp/dist/* sshnp/web/admin/ cp LICENSE sshnp # codesign for apple - if: ${{ matrix.os == 'macos-13' || matrix.os == 'macos-14' }} @@ -217,6 +205,38 @@ jobs: path: ./tarballs/${{ matrix.output-name }}.tgz if-no-files-found: error + web_build: + needs: verify_tags + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: multibuild-${{github.run_number}} + - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version: '20.17.0' + # setup required npm version + - run: | + npm install -g npm@10.8.2 + # create directory needed for build + - name: build admin webapp + working-directory: ./apps/admin/webapp + run: | + mkdir -p sshnp/web/admin + mkdir tarball + npm ci + npm run build + cp -r dist/* sshnp/web/admin/ + tar -cvzf tarball/sshnp-web-admin-noarch.tgz sshnp + # upload the build + - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: + sshnp-web-admin-noarch-${{github.ref_name}}-${{github.run_number + }}-${{github.run_attempt}} + path: ./apps/admin/webapp/tarball + if-no-files-found: error + universal_sh: if: startsWith(github.ref, 'refs/tags/v') defaults: @@ -261,7 +281,7 @@ jobs: github-release: name: >- Upload artifacts and generate SBOMs and checksums for provenance - needs: [main_build, other_build, universal_sh, universal_ps1] + needs: [main_build, other_build, web_build, universal_sh, universal_ps1] runs-on: ubuntu-latest outputs: hashes: ${{ steps.hash.outputs.hashes }} @@ -275,17 +295,21 @@ jobs: with: sparse-checkout: packages/dart/sshnoports/pubspec.lock sparse-checkout-cone-mode: false - - name: Install Syft - uses: anchore/sbom-action/download-syft@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7 - name: Download all the tarballs uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: tarballs/ - - name: Generate SBOMs - run: | - syft scan file:./packages/dart/sshnoports/pubspec.lock \ - -o 'spdx-json=tarballs/dart_sshnoports_sbom.spdx.json' \ - -o 'cyclonedx-json=tarballs/dart_sshnoports_sbom.cyclonedx.json' + - name: Generate SBOM + uses: sbomify/github-action@a04e82ca42a0d9e6bdb57a2cb1a8978e96b4f45c # v0.3.0 + env: + TOKEN: ${{ secrets.SBOMIFY_TOKEN }} + COMPONENT_ID: '-93khk8pUi' + LOCK_FILE: './packages/dart/sshnoports/pubspec.lock' + SBOM_VERSION: ${{github.ref_name}} + OUTPUT_FILE: 'tarballs/noports_dart-${{github.ref_name}}-sbom.cdx.json' + AUGMENT: true + ENRICH: true + UPLOAD: true - name: Move packages for signing run: | cd tarballs @@ -302,7 +326,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} # Upload to GitHub Release using the `gh` CLI. # `tarballs/` contains the built packages, and the - # Syft produced SBOMs + # sbomify produced SBOMs run: >- gh release upload '${{ github.ref_name }}' tarballs/** --repo '${{ github.repository }}' @@ -330,7 +354,6 @@ jobs: name: Clean up temporary branch needs: [main_build, other_build] runs-on: ubuntu-latest - if: ${{ always() }} permissions: contents: write # Needed to delete workflow branch steps: diff --git a/.github/workflows/python-sshnpd-build-publish.yml b/.github/workflows/python-sshnpd-build-publish.yml index 6e4ae2448..e7bb96b72 100644 --- a/.github/workflows/python-sshnpd-build-publish.yml +++ b/.github/workflows/python-sshnpd-build-publish.yml @@ -125,13 +125,17 @@ jobs: with: name: sshnpd-python-package path: dist/ - - name: Install Syft - uses: anchore/sbom-action/download-syft@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7 - - name: Generate SBOMs - run: | - syft scan file:./packages/python/sshnpd/requirements.txt \ - -o 'spdx-json=dist/python_sshnpd_sbom.spdx.json' \ - -o 'cyclonedx-json=dist/python_sshnpd_sbom.cyclonedx.json' + - name: Generate SBOM + uses: sbomify/github-action@a04e82ca42a0d9e6bdb57a2cb1a8978e96b4f45c # v0.3.0 + env: + TOKEN: ${{ secrets.SBOMIFY_TOKEN }} + COMPONENT_ID: 'jqh6pn8rti' + LOCK_FILE: './packages/python/sshnpd/requirements.txt' + SBOM_VERSION: ${{github.ref_name}} + OUTPUT_FILE: 'dist/noports_python-${{github.ref_name}}-sbom.cdx.json' + AUGMENT: true + ENRICH: true + UPLOAD: true - name: Generate SHA256 checksums working-directory: dist run: sha256sum * > checksums.txt diff --git a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart index 4323392a5..21d0de571 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart @@ -153,12 +153,14 @@ class SshnpdParams { aliases: const ['keyFile'], help: 'Sending atSign\'s keyFile if not in ~/.atsign/keys/', ); + parser.addOption( 'atsign', abbr: 'a', mandatory: true, help: 'atSign of this device', ); + parser.addOption( 'managers', aliases: ['manager'], @@ -171,6 +173,7 @@ class SshnpdParams { ' the daemon will check with the --policy-manager atSign re ' ' requests which come from atSigns not in the --managers list.', ); + parser.addOption( 'policy-manager', abbr: 'p', @@ -182,6 +185,7 @@ class SshnpdParams { ' the daemon will check with the --policy-manager atSign re ' ' requests which come from atSigns not in the --managers list.', ); + parser.addOption( 'device', abbr: 'd', @@ -199,6 +203,7 @@ class SshnpdParams { help: 'When set, will update authorized_keys' ' to include public key sent by manager', ); + parser.addFlag( 'hide', abbr: 'h', @@ -208,6 +213,7 @@ class SshnpdParams { ' atSign. Even with this enabled, sshnpd will still respond to ping' ' requests from the manager. (This takes priority over -u / --un-hide)', ); + parser.addFlag( 'un-hide', abbr: 'u', @@ -222,21 +228,24 @@ class SshnpdParams { } }, ); + parser.addFlag( 'verbose', abbr: 'v', help: 'More logging', ); - parser.addOption('ssh-client', - mandatory: false, - defaultsTo: DefaultSshnpdArgs.sshClient.toString(), - allowed: SupportedSshClient.values - .map( - (c) => c.toString(), - ) - .toList(), - help: 'What to use for outbound ssh connections.'); + parser.addOption( + 'ssh-client', + mandatory: false, + defaultsTo: DefaultSshnpdArgs.sshClient.toString(), + allowed: SupportedSshClient.values + .map( + (c) => c.toString(), + ) + .toList(), + help: 'What to use for outbound ssh connections.', + ); parser.addOption( 'root-domain', @@ -258,10 +267,11 @@ class SshnpdParams { parser.addOption( 'local-sshd-port', - help: 'port on which sshd is listening locally on localhost', - defaultsTo: DefaultSshnpdArgs.localSshdPort.toString(), mandatory: false, + defaultsTo: DefaultSshnpdArgs.localSshdPort.toString(), + help: 'port on which sshd is listening locally on localhost', ); + parser.addOption( 'sshpublickey-permissions', abbr: 'S', @@ -270,13 +280,16 @@ class SshnpdParams { 'When --sshpublickey is enabled, will include the specified permissions' ' in the public key entry in authorized_keys', ); - parser.addOption('ephemeral-permissions', - help: 'The permissions which will be added to the authorized_keys file' - ' for the ephemeral public keys which are generated when a client' - ' is connecting via forward ssh' - ' e.g. PermitOpen="host-1:3389",PermitOpen="localhost:80"', - defaultsTo: '', - mandatory: false); + + parser.addOption( + 'ephemeral-permissions', + mandatory: false, + defaultsTo: '', + help: 'The permissions which will be added to the authorized_keys file' + ' for the ephemeral public keys which are generated when a client' + ' is connecting via forward ssh' + ' e.g. PermitOpen="host-1:3389",PermitOpen="localhost:80"', + ); parser.addOption( 'ssh-algorithm', diff --git a/packages/dart/npt_flutter/lib/features/profile/bloc/profile_bloc.dart b/packages/dart/npt_flutter/lib/features/profile/bloc/profile_bloc.dart index 4c1167bab..24ca5ae11 100644 --- a/packages/dart/npt_flutter/lib/features/profile/bloc/profile_bloc.dart +++ b/packages/dart/npt_flutter/lib/features/profile/bloc/profile_bloc.dart @@ -52,6 +52,12 @@ class ProfileBloc extends LoggingBloc { profile = null; } + if (event.copyFrom != null) { + var json = event.copyFrom!.toJson(); + json["uuid"] = uuid; + profile = Profile.fromJson(json); + } + if (profile == null) { emit(ProfileLoaded( uuid, diff --git a/packages/dart/npt_flutter/lib/features/profile/bloc/profile_event.dart b/packages/dart/npt_flutter/lib/features/profile/bloc/profile_event.dart index 9804a9881..d2389d37e 100644 --- a/packages/dart/npt_flutter/lib/features/profile/bloc/profile_event.dart +++ b/packages/dart/npt_flutter/lib/features/profile/bloc/profile_event.dart @@ -18,7 +18,8 @@ final class ProfileLoadEvent extends ProfileEvent { } final class ProfileLoadOrCreateEvent extends ProfileEvent { - const ProfileLoadOrCreateEvent(); + final Profile? copyFrom; + const ProfileLoadOrCreateEvent({this.copyFrom}); @override String toString() { diff --git a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_popup_menu_button.dart b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_popup_menu_button.dart index f0818af8b..fad029199 100644 --- a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_popup_menu_button.dart +++ b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_popup_menu_button.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:npt_flutter/app.dart'; import 'package:npt_flutter/features/profile/profile.dart'; +import 'package:npt_flutter/pages/profile_form_page.dart'; import 'package:npt_flutter/features/profile_list/bloc/profile_list_bloc.dart'; import 'package:npt_flutter/styles/sizes.dart'; import 'package:npt_flutter/widgets/custom_snack_bar.dart'; @@ -10,6 +11,7 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../../routes.dart'; import '../../../util/export.dart'; +import '../../../util/uuid.dart'; import '../../../widgets/confirmation_dialog.dart'; import '../../../widgets/multi_select_dialog.dart'; @@ -41,10 +43,28 @@ class ProfilePopupMenuButton extends StatelessWidget { } if (context.mounted) { - Navigator.of(context).pushNamed(Routes.profileForm, arguments: state.profile.uuid); + Navigator.of(context) + .pushNamed(Routes.profileForm, arguments: ProfileFormPageArguments(state.profile.uuid)); } }, ), + PopupMenuItem( + child: Row( + children: [ + PhosphorIcon(PhosphorIcons.copy()), + gapW10, + const Text("Duplicate"), // TODO: localizations + ], + ), + onTap: () { + var state = context.read().state; + if (state is! ProfileLoadedState) return; + var copyFrom = state.profile; + if (context.mounted) { + Navigator.of(context).pushNamed(Routes.profileForm, + arguments: ProfileFormPageArguments(Uuid.generate(), copyFrom: copyFrom)); + } + }), PopupMenuItem( child: Row( children: [ diff --git a/packages/dart/npt_flutter/lib/features/profile_form/view/profile_form_view.dart b/packages/dart/npt_flutter/lib/features/profile_form/view/profile_form_view.dart index 7f1ede75c..96004b971 100644 --- a/packages/dart/npt_flutter/lib/features/profile_form/view/profile_form_view.dart +++ b/packages/dart/npt_flutter/lib/features/profile_form/view/profile_form_view.dart @@ -8,7 +8,8 @@ import 'package:npt_flutter/widgets/custom_card.dart'; class ProfileFormView extends StatelessWidget { final String uuid; - const ProfileFormView(this.uuid, {super.key}); + final Profile? copyFrom; + const ProfileFormView(this.uuid, {super.key, this.copyFrom}); @override Widget build(BuildContext context) { @@ -19,7 +20,7 @@ class ProfileFormView extends StatelessWidget { create: (BuildContext context) => /// Local copy of the profile which is used by the form - ProfileBloc(context.read(), uuid)..add(const ProfileLoadOrCreateEvent()), + ProfileBloc(context.read(), uuid)..add(ProfileLoadOrCreateEvent(copyFrom: copyFrom)), child: Padding( padding: const EdgeInsets.only(left: Sizes.p100, right: Sizes.p100), child: Stack( diff --git a/packages/dart/npt_flutter/lib/features/profile_list/widgets/profile_list_add_button.dart b/packages/dart/npt_flutter/lib/features/profile_list/widgets/profile_list_add_button.dart index f353afeba..b17843524 100644 --- a/packages/dart/npt_flutter/lib/features/profile_list/widgets/profile_list_add_button.dart +++ b/packages/dart/npt_flutter/lib/features/profile_list/widgets/profile_list_add_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:npt_flutter/routes.dart'; +import 'package:npt_flutter/pages/profile_form_page.dart'; import 'package:npt_flutter/styles/sizes.dart'; import 'package:npt_flutter/util/uuid.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -25,7 +26,7 @@ class ProfileListAddButton extends StatelessWidget { onPressed: () { final uuid = Uuid.generate(); if (context.mounted) { - Navigator.of(context).pushNamed(Routes.profileForm, arguments: uuid); + Navigator.of(context).pushNamed(Routes.profileForm, arguments: ProfileFormPageArguments(uuid)); } }, label: Text(strings.addNew), diff --git a/packages/dart/npt_flutter/lib/pages/profile_form_page.dart b/packages/dart/npt_flutter/lib/pages/profile_form_page.dart index 9b19939a6..75723241b 100644 --- a/packages/dart/npt_flutter/lib/pages/profile_form_page.dart +++ b/packages/dart/npt_flutter/lib/pages/profile_form_page.dart @@ -1,18 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:npt_flutter/features/profile_form/profile_form.dart'; +import 'package:npt_flutter/features/profile/models/profile.dart'; import 'package:npt_flutter/widgets/npt_app_bar.dart'; +class ProfileFormPageArguments { + final String uuid; + final Profile? copyFrom; + ProfileFormPageArguments(this.uuid, {this.copyFrom}); +} + class ProfileFormPage extends StatelessWidget { const ProfileFormPage({super.key}); @override Widget build(BuildContext context) { - final uuid = ModalRoute.of(context)!.settings.arguments as String; + final args = ModalRoute.of(context)!.settings.arguments as ProfileFormPageArguments; final strings = AppLocalizations.of(context)!; return Scaffold( appBar: NptAppBar(title: strings.addNewProfile), - body: ProfileFormView(uuid), + body: ProfileFormView(args.uuid, copyFrom: args.copyFrom), ); } } diff --git a/packages/dart/npt_flutter/macos/Podfile.lock b/packages/dart/npt_flutter/macos/Podfile.lock index e47331eb8..baec81b66 100644 --- a/packages/dart/npt_flutter/macos/Podfile.lock +++ b/packages/dart/npt_flutter/macos/Podfile.lock @@ -87,4 +87,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/packages/dart/npt_flutter/pubspec.lock b/packages/dart/npt_flutter/pubspec.lock index 3a7b544e4..9a1048c72 100644 --- a/packages/dart/npt_flutter/pubspec.lock +++ b/packages/dart/npt_flutter/pubspec.lock @@ -939,7 +939,7 @@ packages: path: "../noports_core" relative: true source: path - version: "6.2.0" + version: "6.2.1" openssh_ed25519: dependency: transitive description: @@ -1317,10 +1317,10 @@ packages: dependency: "direct main" description: name: socket_connector - sha256: "3c641546699aa58e9ab8be9841627a30af3c1ffcf4461ca5d00d7c56392ab63a" + sha256: "091c83fb214f6ff48dbf38f6e011e148d996cce05487303c5e8a0cd72369b0e2" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.3" source_gen: dependency: transitive description: