feat(store): trust gate before every module install
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
One-click installs showed no trust signal at all (top security finding of the usertest panel). Every install path — store card, detail sheet, and the flow list's quick fix — now routes through one confirmation dialog showing what the hub actually knows before download: origin store, version, license, maturity, and required services/capabilities, plus the sandbox model and an honest note that per-entry signature status is not in the store index yet (verification happens hub-side at install). Widget tests cover content and confirm/cancel semantics. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
c253f5d23e
commit
039f11b6bc
10 changed files with 477 additions and 0 deletions
|
|
@ -1565,6 +1565,18 @@
|
|||
"aboutCopyTooltip": "Kopieren",
|
||||
"aboutCopiedToast": "In die Zwischenablage kopiert.",
|
||||
"sidebarSearchLabel": "Suchen & Befehle",
|
||||
"installConfirmTitle": "„{name}\" installieren?",
|
||||
"@installConfirmTitle": {"placeholders": {"name": {"type": "String"}}},
|
||||
"installConfirmVersion": "Version",
|
||||
"installConfirmSource": "Quelle",
|
||||
"installConfirmSourceBundled": "Mitgelieferter Store-Index",
|
||||
"installConfirmLicense": "Lizenz",
|
||||
"installConfirmStatus": "Reifegrad",
|
||||
"installConfirmNeedsServices": "Benötigte Dienste",
|
||||
"installConfirmNeedsCapabilities": "Benötigte Fähigkeiten",
|
||||
"installConfirmTrustTitle": "Vertrauen & Sicherheit",
|
||||
"installConfirmTrustBody": "Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.",
|
||||
"installConfirmSignatureNote": "Signaturen prüft der Hub beim Installieren, sofern die Signaturpflicht im Sicherheitsprofil aktiv ist. Einen Signaturstatus vorab zeigt der Store-Index derzeit noch nicht an (Alpha).",
|
||||
"settingsSidebarPinnedTitle": "Navigation immer ausgeklappt",
|
||||
"settingsSidebarPinnedBody": "Zeigt die Beschriftungen der Seitenleiste dauerhaft an, statt sie nur beim Überfahren mit der Maus einzublenden.",
|
||||
"settingsCategoryAi": "System-KI",
|
||||
|
|
|
|||
|
|
@ -1589,6 +1589,18 @@
|
|||
"aboutCopyTooltip": "Copy",
|
||||
"aboutCopiedToast": "Copied to the clipboard.",
|
||||
"sidebarSearchLabel": "Search & commands",
|
||||
"installConfirmTitle": "Install \"{name}\"?",
|
||||
"@installConfirmTitle": {"placeholders": {"name": {"type": "String"}}},
|
||||
"installConfirmVersion": "Version",
|
||||
"installConfirmSource": "Source",
|
||||
"installConfirmSourceBundled": "Bundled store index",
|
||||
"installConfirmLicense": "License",
|
||||
"installConfirmStatus": "Maturity",
|
||||
"installConfirmNeedsServices": "Required services",
|
||||
"installConfirmNeedsCapabilities": "Required capabilities",
|
||||
"installConfirmTrustTitle": "Trust & security",
|
||||
"installConfirmTrustBody": "The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.",
|
||||
"installConfirmSignatureNote": "The hub verifies signatures at install time whenever the security profile requires them. The store index does not yet show a per-entry signature status up front (alpha).",
|
||||
"settingsSidebarPinnedTitle": "Keep the navigation expanded",
|
||||
"settingsSidebarPinnedBody": "Shows the sidebar labels permanently instead of only while hovering with the mouse.",
|
||||
"settingsCategoryAi": "System AI",
|
||||
|
|
|
|||
|
|
@ -4640,6 +4640,72 @@ abstract class AppLocalizations {
|
|||
/// **'Search & commands'**
|
||||
String get sidebarSearchLabel;
|
||||
|
||||
/// No description provided for @installConfirmTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Install \"{name}\"?'**
|
||||
String installConfirmTitle(String name);
|
||||
|
||||
/// No description provided for @installConfirmVersion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Version'**
|
||||
String get installConfirmVersion;
|
||||
|
||||
/// No description provided for @installConfirmSource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Source'**
|
||||
String get installConfirmSource;
|
||||
|
||||
/// No description provided for @installConfirmSourceBundled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bundled store index'**
|
||||
String get installConfirmSourceBundled;
|
||||
|
||||
/// No description provided for @installConfirmLicense.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'License'**
|
||||
String get installConfirmLicense;
|
||||
|
||||
/// No description provided for @installConfirmStatus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Maturity'**
|
||||
String get installConfirmStatus;
|
||||
|
||||
/// No description provided for @installConfirmNeedsServices.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Required services'**
|
||||
String get installConfirmNeedsServices;
|
||||
|
||||
/// No description provided for @installConfirmNeedsCapabilities.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Required capabilities'**
|
||||
String get installConfirmNeedsCapabilities;
|
||||
|
||||
/// No description provided for @installConfirmTrustTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Trust & security'**
|
||||
String get installConfirmTrustTitle;
|
||||
|
||||
/// No description provided for @installConfirmTrustBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.'**
|
||||
String get installConfirmTrustBody;
|
||||
|
||||
/// No description provided for @installConfirmSignatureNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The hub verifies signatures at install time whenever the security profile requires them. The store index does not yet show a per-entry signature status up front (alpha).'**
|
||||
String get installConfirmSignatureNote;
|
||||
|
||||
/// No description provided for @settingsSidebarPinnedTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -2715,6 +2715,43 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get sidebarSearchLabel => 'Suchen & Befehle';
|
||||
|
||||
@override
|
||||
String installConfirmTitle(String name) {
|
||||
return '„$name\" installieren?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get installConfirmVersion => 'Version';
|
||||
|
||||
@override
|
||||
String get installConfirmSource => 'Quelle';
|
||||
|
||||
@override
|
||||
String get installConfirmSourceBundled => 'Mitgelieferter Store-Index';
|
||||
|
||||
@override
|
||||
String get installConfirmLicense => 'Lizenz';
|
||||
|
||||
@override
|
||||
String get installConfirmStatus => 'Reifegrad';
|
||||
|
||||
@override
|
||||
String get installConfirmNeedsServices => 'Benötigte Dienste';
|
||||
|
||||
@override
|
||||
String get installConfirmNeedsCapabilities => 'Benötigte Fähigkeiten';
|
||||
|
||||
@override
|
||||
String get installConfirmTrustTitle => 'Vertrauen & Sicherheit';
|
||||
|
||||
@override
|
||||
String get installConfirmTrustBody =>
|
||||
'Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.';
|
||||
|
||||
@override
|
||||
String get installConfirmSignatureNote =>
|
||||
'Signaturen prüft der Hub beim Installieren, sofern die Signaturpflicht im Sicherheitsprofil aktiv ist. Einen Signaturstatus vorab zeigt der Store-Index derzeit noch nicht an (Alpha).';
|
||||
|
||||
@override
|
||||
String get settingsSidebarPinnedTitle => 'Navigation immer ausgeklappt';
|
||||
|
||||
|
|
|
|||
|
|
@ -2717,6 +2717,43 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get sidebarSearchLabel => 'Search & commands';
|
||||
|
||||
@override
|
||||
String installConfirmTitle(String name) {
|
||||
return 'Install \"$name\"?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get installConfirmVersion => 'Version';
|
||||
|
||||
@override
|
||||
String get installConfirmSource => 'Source';
|
||||
|
||||
@override
|
||||
String get installConfirmSourceBundled => 'Bundled store index';
|
||||
|
||||
@override
|
||||
String get installConfirmLicense => 'License';
|
||||
|
||||
@override
|
||||
String get installConfirmStatus => 'Maturity';
|
||||
|
||||
@override
|
||||
String get installConfirmNeedsServices => 'Required services';
|
||||
|
||||
@override
|
||||
String get installConfirmNeedsCapabilities => 'Required capabilities';
|
||||
|
||||
@override
|
||||
String get installConfirmTrustTitle => 'Trust & security';
|
||||
|
||||
@override
|
||||
String get installConfirmTrustBody =>
|
||||
'The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.';
|
||||
|
||||
@override
|
||||
String get installConfirmSignatureNote =>
|
||||
'The hub verifies signatures at install time whenever the security profile requires them. The store index does not yet show a per-entry signature status up front (alpha).';
|
||||
|
||||
@override
|
||||
String get settingsSidebarPinnedTitle => 'Keep the navigation expanded';
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import '../data/flow_run_driver.dart';
|
|||
import '../data/hub.dart';
|
||||
import '../data/workspace.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../widgets/chain_install_confirm.dart';
|
||||
|
||||
class FlowsPage extends StatefulWidget {
|
||||
/// Pre-load this flow when the editor first builds. Studio
|
||||
|
|
@ -114,6 +115,11 @@ class _FlowsPageState extends State<FlowsPage> {
|
|||
// entry for 'debug.echo@^0'"). Strip it — same as the Store page.
|
||||
final at = capability.indexOf('@');
|
||||
final bare = at < 0 ? capability : capability.substring(0, at);
|
||||
// The quick-fix install goes through the same trust dialog as
|
||||
// the Store page — no quieter direct path.
|
||||
final confirmed =
|
||||
await ChainInstallConfirmDialog.showForCapability(context, bare);
|
||||
if (!confirmed) return null;
|
||||
return _runInstall(source: bare);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -193,6 +193,11 @@ class _StorePageState extends State<StorePage> {
|
|||
|
||||
Future<void> _install(StoreItem item) async {
|
||||
if (!mounted) return;
|
||||
// Trust gate: provenance + sandbox/signature context BEFORE
|
||||
// anything is downloaded (usertest: one-click install with
|
||||
// no visible trust signal was the top security finding).
|
||||
final confirmed = await ChainInstallConfirmDialog.show(context, item);
|
||||
if (!confirmed || !mounted) return;
|
||||
final outcome = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
|
|
@ -2293,6 +2298,11 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
}
|
||||
|
||||
Future<void> _install() async {
|
||||
// Same trust gate as the store card — the detail sheet's
|
||||
// install button must not be a quieter bypass.
|
||||
final confirmed =
|
||||
await ChainInstallConfirmDialog.show(context, widget.item);
|
||||
if (!confirmed || !mounted) return;
|
||||
setState(() {
|
||||
_busy = true;
|
||||
_toast = null;
|
||||
|
|
|
|||
212
lib/widgets/chain_install_confirm.dart
Normal file
212
lib/widgets/chain_install_confirm.dart
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// Install confirmation with provenance + trust context. The
|
||||
// usertest security persona flagged one-click installs with no
|
||||
// visible trust signal as the platform's biggest supply-chain
|
||||
// gap: every install path (store card, detail sheet, flow-list
|
||||
// quick fix) now routes through this dialog, which shows what
|
||||
// the hub actually knows before anything is downloaded — origin
|
||||
// store, version, license, maturity, required services — plus
|
||||
// an honest note on when signature verification happens. No
|
||||
// fake "verified" badge: per-entry signature status is not in
|
||||
// the store index yet, and the dialog says so instead of
|
||||
// pretending.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/hub.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/tokens.dart';
|
||||
|
||||
class ChainInstallConfirmDialog extends StatelessWidget {
|
||||
final StoreItem item;
|
||||
|
||||
const ChainInstallConfirmDialog({super.key, required this.item});
|
||||
|
||||
/// Confirm installing a known store entry. Returns true to
|
||||
/// proceed.
|
||||
static Future<bool> show(BuildContext context, StoreItem item) async {
|
||||
final r = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => ChainInstallConfirmDialog(item: item),
|
||||
);
|
||||
return r == true;
|
||||
}
|
||||
|
||||
/// Confirm installing by bare capability name (the flow-list
|
||||
/// quick-fix path). Looks the entry up in the store index so
|
||||
/// the dialog shows the same provenance as the Store page;
|
||||
/// falls back to a minimal entry when the index has no match
|
||||
/// (the hub will reject unknown names on install anyway).
|
||||
static Future<bool> showForCapability(
|
||||
BuildContext context,
|
||||
String capability,
|
||||
) async {
|
||||
StoreItem? match;
|
||||
try {
|
||||
final hits = await HubService.instance.searchStore(
|
||||
query: capability,
|
||||
limit: 10,
|
||||
);
|
||||
for (final h in hits) {
|
||||
if (h.name == capability) {
|
||||
match = h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Unreachable store index — fall through to the minimal
|
||||
// entry; the dialog still explains the trust model.
|
||||
}
|
||||
if (!context.mounted) return false;
|
||||
return show(
|
||||
context,
|
||||
match ??
|
||||
StoreItem(
|
||||
name: capability,
|
||||
taglineEn: '',
|
||||
taglineDe: '',
|
||||
descriptionEn: '',
|
||||
descriptionDe: '',
|
||||
category: '',
|
||||
tags: const [],
|
||||
requiresCapabilities: const [],
|
||||
requiresServices: const [],
|
||||
license: '',
|
||||
repository: '',
|
||||
bestVersion: '',
|
||||
status: '',
|
||||
installed: false,
|
||||
featured: false,
|
||||
iconUrl: '',
|
||||
screenshotUrls: const [],
|
||||
docsUrl: '',
|
||||
kind: '',
|
||||
provider: '',
|
||||
sourceKind: '',
|
||||
source: '',
|
||||
canonicalCategory: '',
|
||||
canonicalCategoryLabel: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _statusLabel(AppLocalizations l) => switch (item.status) {
|
||||
'published' => l.storeStatusPublished,
|
||||
'alpha' => l.storeStatusAlpha,
|
||||
'planned' => l.storeStatusPlanned,
|
||||
_ => item.status,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final rows = <(String, String)>[
|
||||
if (item.bestVersion.isNotEmpty)
|
||||
(l.installConfirmVersion, 'v${item.bestVersion}'),
|
||||
(
|
||||
l.installConfirmSource,
|
||||
item.source.isEmpty || item.source == 'bundled'
|
||||
? l.installConfirmSourceBundled
|
||||
: item.source,
|
||||
),
|
||||
if (item.license.isNotEmpty) (l.installConfirmLicense, item.license),
|
||||
if (item.status.isNotEmpty) (l.installConfirmStatus, _statusLabel(l)),
|
||||
if (item.requiresServices.isNotEmpty)
|
||||
(l.installConfirmNeedsServices, item.requiresServices.join(', ')),
|
||||
if (item.requiresCapabilities.isNotEmpty)
|
||||
(
|
||||
l.installConfirmNeedsCapabilities,
|
||||
item.requiresCapabilities.join(', ')
|
||||
),
|
||||
];
|
||||
return AlertDialog(
|
||||
title: Text(l.installConfirmTitle(item.name)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ChainRadius.md),
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 460),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final (label, value) in rows)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: ChainSpace.xs),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(value, style: theme.textTheme.bodySmall),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: ChainSpace.sm),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ChainSpace.md),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(ChainRadius.sm),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.shield_outlined,
|
||||
size: 14,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
l.installConfirmTrustTitle,
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: ChainSpace.xs),
|
||||
Text(
|
||||
l.installConfirmTrustBody,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: ChainSpace.xs),
|
||||
Text(
|
||||
l.installConfirmSignatureNote,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(l.buttonInstall),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ export 'chain_empty_state.dart';
|
|||
export 'chain_en_badge.dart';
|
||||
export 'chain_error_box.dart';
|
||||
export 'chain_flow_output.dart';
|
||||
export 'chain_install_confirm.dart';
|
||||
export 'chain_log_viewer.dart';
|
||||
export 'chain_module_sheet.dart';
|
||||
export 'chain_pill.dart';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue