runcloud 0.2.167 → 0.2.169

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,7 @@ const JUDGMENT_IDS = new Set([
30
30
  'background_mode_justification',
31
31
  'push_notification_setup',
32
32
  'deep_link_config',
33
+ 'rating_prompt',
33
34
  ]);
34
35
  const LIVE_STORE_IDS = new Set([
35
36
  'apple_privacy_policy',
@@ -210,6 +211,8 @@ function evaluateLocalItem(root, appJson, item) {
210
211
  return evaluatePermissionDescriptions(root, appJson, item);
211
212
  case 'privacy_manifest':
212
213
  return evaluatePrivacyManifest(root, appJson, item);
214
+ case 'rating_prompt':
215
+ return evaluateRatingPrompt(root, item);
213
216
  default:
214
217
  if (STORE_REMINDER_IDS.has(item.id)) {
215
218
  return makeItem(item, 'info', 'Complete this declaration in Google Play Console.', 'Play Console questionnaire');
@@ -259,6 +262,12 @@ function evaluateRestorePurchases(root, item) {
259
262
  return makeItem(item, 'pass', 'Restore purchases flow appears in source.', evidence(restore));
260
263
  return makeItem(item, 'fail', 'IAP APIs found but no restore purchases flow was found.', evidence(iap));
261
264
  }
265
+ function evaluateRatingPrompt(root, item) {
266
+ const asks = scan(root, /expo-store-review|react-native-in-app-review|requestReview|SKStoreReviewController/);
267
+ if (asks.length)
268
+ return null;
269
+ return makeItem(item, 'warning', 'The app never asks people for a rating.', 'scan: no store rating prompt found');
270
+ }
262
271
  const TRACKING_SDK = /["'](?:@react-native-firebase\/analytics|react-native-fbsdk(?:-next)?|expo-facebook|react-native-appsflyer|@amplitude\/[\w.-]+|@segment\/[\w.-]+)(?:\/[^"']*)?["']/;
263
272
  function evaluateTrackingWithoutAtt(root, item) {
264
273
  const tracking = scan(root, TRACKING_SDK);
@@ -145,8 +145,10 @@ export function hostReport(host, canonical, extra, diagnoses, now, fleetTtl, dee
145
145
  processScan: Object.keys(process).length ? {
146
146
  observedAt: stamp(process.observedAt), staleAfterSeconds: num(process.staleAfterSeconds) ?? 900,
147
147
  sourceUnavailable: process.sourceUnavailable === true || Boolean(extra.collectorError), status: str(process.status) ?? 'unknown', error: str(process.error), root: process.root === true,
148
- iocs: rows(process.iocs).map(ioc => ({ status: str(ioc.status) ?? 'unknown', contained: ioc.contained === true })),
149
- findings: rows(process.findings).map(finding => ({ reason: str(finding.reason) ?? 'unknown' })),
148
+ iocs: rows(process.iocs).map(ioc => ({ status: str(ioc.status) ?? 'unknown', contained: ioc.contained === true,
149
+ signals: Array.isArray(ioc.signals) ? ioc.signals.filter((signal) => typeof signal === 'string') : [], lastObservedAt: stamp(ioc.lastObservedAt) })),
150
+ findings: rows(process.findings).map(finding => ({ reason: str(finding.reason) ?? 'unknown', confidence: str(finding.confidence) ?? undefined,
151
+ lastObservedAt: stamp(finding.lastObservedAt) })),
150
152
  coverage: { iocs: str(row(process.coverage).iocs) ?? 'partial' }, baseline: { state: str(row(process.baseline).state) ?? 'missing' },
151
153
  } : null,
152
154
  };
@@ -10,6 +10,22 @@ export function processFreshness(scan, now) {
10
10
  return 'stale';
11
11
  return 'fresh';
12
12
  }
13
+ export function processSecurityAlert(scan, held = false) {
14
+ if (!scan)
15
+ return null;
16
+ const strongIocs = (scan.iocs ?? []).filter(ioc => ['present', 'active'].includes(ioc.status) && ((ioc.signals ?? []).some(signal => ['binary_hash', 'running_hash'].includes(signal)) ||
17
+ ((ioc.signals ?? []).includes('original_path') && (ioc.signals ?? []).some(signal => ['plist_hash', 'loaded_label', 'root_daemon', 'running_path'].includes(signal)))));
18
+ const knownFindings = (scan.findings ?? []).filter(finding => finding.reason === 'known_ioc' && finding.confidence === 'high');
19
+ const contained = (scan.iocs ?? []).filter(ioc => ioc.contained || ioc.status === 'contained');
20
+ const detected = scan.status === 'critical' || strongIocs.length > 0 || knownFindings.length > 0;
21
+ if (!detected && (!held || !contained.length))
22
+ return null;
23
+ const timestamps = (detected ? [...strongIocs.map(ioc => ioc.lastObservedAt), ...knownFindings.map(finding => finding.lastObservedAt)]
24
+ : contained.map(ioc => ioc.lastObservedAt)).filter((value) => !!value && Number.isFinite(Date.parse(value)));
25
+ const observedAt = timestamps.sort((a, b) => Date.parse(b) - Date.parse(a))[0] ?? scan.observedAt;
26
+ return detected ? { state: 'detected', label: 'Known malware detected', observedAt }
27
+ : { state: 'contained-hold', label: 'Malware contained · hold reported', observedAt };
28
+ }
13
29
  export function leaseStateLabel(state) {
14
30
  return { accepting: 'Lease gate open', rejecting: 'Lease gate closed', unknown: 'Lease gate unknown', held: 'Operator hold' }[state] ?? state;
15
31
  }
@@ -102,11 +118,12 @@ export function machineDiagnosis(host, hosted, observedState, now, staleAfterSec
102
118
  checks.push('process checks missing');
103
119
  else {
104
120
  const freshness = processFreshness(scan, now);
105
- const malicious = scan.iocs.some(ioc => ['present', 'active'].includes(ioc.status)) || scan.findings.some(finding => finding.reason === 'known_ioc');
121
+ const alert = processSecurityAlert(scan);
122
+ const malicious = alert?.state === 'detected';
106
123
  if (malicious)
107
- findings.unshift(`known malicious daemon detected${freshness === 'fresh' ? '' : ' in retained evidence; recheck required'}`);
108
- else if (scan.status === 'critical')
109
- findings.unshift('critical process finding requires review');
124
+ findings.unshift(`known malicious daemon detected${processFreshness({ ...scan, observedAt: alert.observedAt }, now) === 'fresh' ? '' : ' in retained evidence; recheck required'}`);
125
+ else if (scan.iocs.some(ioc => ['present', 'active'].includes(ioc.status)))
126
+ checks.push('process IOC signals need corroboration; see scanner details');
110
127
  const review = scan.findings.filter(finding => ['rooted_workload', 'long_lived_workload', 'identity_changed', 'unreviewed_persistence'].includes(finding.reason));
111
128
  if (review.length)
112
129
  findings.push(`${review.length} process or persistence finding(s) need review`);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.2.167';
1
+ export const CLI_VERSION = '0.2.169';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.2.167",
3
+ "version": "0.2.169",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
Binary file