fleet icon indicating copy to clipboard operation
fleet copied to clipboard

Patch software: install Fleet-maintained app if host is below the current version

Open eugkuo opened this issue 1 year ago • 74 comments

Goal

User story
As an IT admin on the Software title page for software available as a Fleet-maintained app (macOS and Windows),
I want to be able to patch the software if it's below the current version
so that I can quickly fix vulnerabilities.

Key result

Auto-update (patch) any software without writing custom policies

Original requests

  • #21825

Context

  • Product designer: @eugkuo
  • Engineering support: @iansltx

Changes

Product

  • [ ] UI changes: Figma link
  • [ ] CLI (fleetctl) usage changes: None
  • [ ] YAML changes: None
  • [ ] REST API changes: #27442 & #27565
  • [ ] Fleet's agent (fleetd) changes: None
  • [ ] Activity changes: See Figma here and #27683
  • [ ] Permissions changes: None
  • [ ] Changes to paid features or tiers: Fleet Premium only
  • [ ] Transparency changes: None
  • [ ] First draft of test plan added
  • [ ] Other reference documentation changes: None
  • [ ] Once shipped, requester has been notified
  • [ ] Once shipped, dogfooding issue has been filed

Engineering

  • [ ] Test plan is finalized
  • [ ] Feature guide changes: TODO
  • [ ] Database schema migrations: TODO
  • [ ] Load testing: TODO

ℹ️  Please read this issue carefully and understand it. Pay special attention to UI wireframes, especially "dev notes".

QA

Risk assessment

  • Requires load testing: TODO
  • Risk level: Low / High TODO
  • Risk description: TODO

Test plan

Migration

  • The plan is for #g-orchestration to build the “Custom targets (labels) for policies” user story (#24097) at the same time so, as part of this #g-software story, we want a migration to move scope from software over to policies.

Patch

  1. On the software list page find an app that exists in the list of available Fleet-maintained apps that has been installed by end-users but for which no package has been added (for ex, Zoom).
  2. Select the software to go to the software details page.
  3. Select 'Patch' from the Actions dropdown.
  4. Verify that a modal appears that displays the installer package.
    1. The 'Show details' link should close the 'Patch' modal and open the 'Details' modal. Closing the Details modal will reopen the Patch modal.
  5. Select 'Add software'
  6. Verify the user is taken to the software detail page with the package added to the page and that a flash message appears verifying that the package is now available.
  7. Select 'patch' from the Actions dropdown.
  8. Verify that the 'Patch' software modal opens.
  9. Enable patching and select 'save'
    1. Verify that a flash message appears to confirm that the patch software policy has been edited.
  10. Verify that the software detail page updates with a 'Patch policy.
    1. A Fleet icon should indicate that this is a fleet-created policy.
    2. Rolling over the icon should show a tooltip to this effect.
    3. Selecting anywhere on the row of a policy title opens the policy modal.
  11. Verify that the software is patched where it's installed.

Install

  1. On the software list page find an app that has a Fleet-maintained installer package that has been installed by users but for which no package has been added.
  2. Select the software to go to the software details page.
  3. Select 'Install' from the Actions dropdown.
  4. Verify that a modal appears that displays the installer package.
    1. The 'Show details' link should close the 'Install' modal and open the 'Details' modal. Closing the Details modal will reopen the Patch modal.
  5. Select 'Add software'
  6. Verify the user is taken to the software detail page with the package added to the page and that a flash message appears verifying that the package is now available.
  7. Select 'Install' from the Actions dropdown.
  8. Verify that the 'Install' software modal opens.
  9. Enable Install and select 'Save'
  10. Verify that a flash message appears to confirm that the install software policy has been edited.
  11. Verify that the software detail page updates with an 'Install policy.
    1. A Fleet icon should indicate that this is a fleet-created policy.
    2. Rolling over the icon should show a tooltip to this effect.
    3. Selecting anywhere on the row of a policy title opens the policy modal.
  12. Verify that the software is installed on the targeted hosts.

Enable self-service

  1. On the software list page find an app that has a Fleet-maintained installer package that has been installed by users but for which no package has been added.
  2. Select the software to go to the software details page.
  3. Select 'Enable self-service' from the Actions dropdown.
  4. Verify that a modal appears that displays the installer package.
    1. The 'Show details' link should close the 'Install' modal and open the 'Details' modal. Closing the Details modal will reopen the Patch modal.
  5. Select 'Add software'
  6. Verify the user is taken to the software detail page with the package added to the page and that a flash message appears verifying that the package is now available.
  7. Select 'Enable self-service' from the Actions dropdown.
  8. Verify that the 'Enable self-service' modal opens.
  9. Enable Self-service and select 'Save'
  10. Verify that a flash message appears to confirm that the software has been made available for self-service.
  11. Verify that the software shows up in self-service for the targeted hosts.
  12. Verify that 'Enable' has changed to 'Disable' in the 'Actions' dropdown.
  13. Select 'Disable self-service.'
  14. From the modal that appears select 'save'
  15. Verify that the app has been removed from self-service on targeted hosts.

Edit installer package

  1. From the software details page select 'edit (the pencil icon)'.
  2. Verify that a modal opens that allows you to change the installer package and shows advanced options only.

Delete installer package

  1. From the software details page select 'delete (the pencil icon)'.
  2. Verify that the delete confirmation modal appears.
  3. Seelct 'save'
  4. Verify that updates/installs have ceased in the future.

No installer package available

  1. From the software details page select an app that is not a FMA with no installer package.
  2. From the 'actions' menu select 'install'
  3. Verify that a modal appears indicating that there is no app to install.
  4. Select 'add software'
  5. Verify that you are taken to the 'Add software' page.
  6. Verify that adding any software from this page will take you to the software details page of that app with the installer package listed.
  7. Repeat above, select 'patch' from the 'actions' menu.
  8. Repeat above, selecting 'enable self-service,' but verify that the modal reads 'Enable self-service'.

Delete software with a custom policy attached

  1. Delete software with a custom policy attached to it.
  2. Ensure the software is removed from the custom policy's 'install software' policy automations.
    1. Also ensure that the previouslyshown flash message telling IT admins they can't delete software if a custom policy is attached to it is removed.

Policy, install, policy failure 1.. We won’t change behavior if a policy fails, software install fails, and then policy fails again. We won’t retry the install if the policy stays failing.

Hiding policies

  1. Verify the Fleet-created install/patch policies don't appear on the Policies page.
  2. Verify the Fleet-created install/patch policies don't appear on the My devices page.
    1. Verify the Fleet-created install/patch policies don't appear on the Policies > Manage automations: Run script, Install software, Calendars, and Other workflows.

Add custom package

  1. Navigate to add a custom package page
  2. Ensure Options and targeting are removed.

Add App Store (VPP) app

  1. Navigate to add an App Store (VPP) app page
  2. Ensure Options and targeting are removed.

Activity feed

  1. Verify that we are showing the user who initiated the activity even tho this is a Fleet-maintained activity. See figma

Additional

  1. For non-Fleet-maintained apps verify "patch" is shown in the "Actions" menu on the software details page with an associated installer package, but is greyed out with a tool tip.
  2. Verify a Fleet-maintained or App Store (VPP) apps have a "Fleet-maintained" or "App Store (VPP)" label is shown in the installer package metadata for Fleet-maintained and App Store (VPP) apps.
    1. Verify that Custom packages do not have a label.

UI Changes Software details page changes: Versions is in section under app name Install statuses and policies are down lower Make sure all the links and host counts work Make sure pagination of policies works Is there versions pagination?

Software list (Available for install) /software/titles and host >> software: Should have same icon as Automatic install (and combo icon if also self service?) - icon hover text should be updated

Deleting software

  1. Deleting installer package deletes associated fleet-created policies
  2. Deleting installer detaches software from any custom policy automations

Policy behavior changes

  1. If a policy records a failure, software fails to install and then the policy fails again, software install will be reattempted

Scoping changes Where can I set scope? Self service, automatically install, patch? Where is scope set? Is it on the package? Is it on the method of install?

Upgrading existing software packages, policies, etc Will these be automatically "converted" or is it only things added after upgrading?

API changes Should be able to add new software with minimum_version flag Figure out if POST with minimum_version flag only works for initial upload or if it also works as edit for FMA, VPP, Custom packages. Looks like FMA, custom packages and VPP apps all support GET, POST, PATCH. What could I do before but maybe gets messed up now if I try with minimum_version?

  • minimum_version set x.x -> minimum_version set x.x
  • minimum_version not set -> minimum_version set x.x
  • minimum_version set x.x -> minimum_version not set
  • minimum_version set x.x -> minimum_version set x.y

Documentation In addition to the patch workflows, we'll need to update any documentation that states software install only happens for policies going from no result -> fail or pass -> fail. Also API docs.

Testing notes

Confirmation

  1. [ ] Engineer: Added comment to user story confirming successful completion of test plan.
  2. [ ] QA: Added comment to user story confirming successful completion of test plan.

eugkuo avatar Jan 16 '25 12:01 eugkuo

Notes from call with @eugkuo:

  • Noah: Let's design editing "Automatic install"

  • Noah: Let's design "Latest"

  • Noah: For all software (Fleet-maintained, App Store, custom packages) Fleet will write a pre-install query to fail if the app is running. Why? Installing while end user is using the app is a poor experience.

    • Noah: Update the automatic install behavior such that if the pre-install query fails then Fleet will attempt the install again (if policy is failing). Wait on making this behavior configurable.
  • Noah: Error message for failed to install self-service app in Fleet Desktop is unclear. Reason was I needed to refresh My device page (token expired)

    • TODO: Noah: File a feature request for a better error message
  • TODO: Noah: Update copy to instruct end user to refresh the page when pending: Image

noahtalerman avatar Feb 18 '25 16:02 noahtalerman

Comments from 20 Feb 25 review:

  1. Tim: If the Admin enters a version higher than latest, Fleet will trigger installs in perpetuity. Consider some kind of validation here.
    1. Noah: No validation for now.
  2. Rachel: What if we just put a warning if it’s not semantic versioning? e.g. Warning: This minimum version requirement is not using semantic versioning and may not be correct.
  3. Rachel: Consider separating 1) automatic install when not present and 2) when minimum version requirement is not met: [ ] Automatically install app if not present on workstation [ ] Automatically update app if not meeting minimum version requirement Minimum version required: ( ) Specific version: [____________] (e.g., 1.2.3) ( ) Latest version available
  4. Tim: need to do some research on auto-creating pre-install query. Since the intent is to not install if the app is running, we should be fairly confident that app name == process name.

eugkuo avatar Feb 19 '25 17:02 eugkuo

Riffing on what @RachelElysia mentioned, we could have a UX with non-nested checkboxes with the heading "Automatically install when":

  • [ ] Software is not installed
  • [ ] Software is not up to date

The above can run as separate, simpler, policies and covers the “ensure: upgraded” use case for YAML for free.

Implied in the above is deferring custom minimum version constraints in the UI, but if a customer has explicitly asked for that (such that uploaded software version isn't enough, and editing the policy after the fact isn't enough), fair enough.

There's also an interaction here with VPP apps where we're bumping the version to latest hourly as of 4.65, so if we do this for VPP we either have to be very clear about what we aren't doing (e.g. "create a policy to automatically install when" rather than "automatically install when") or auto-update policies when the underlying information changes.

This also gets us into the realm of really needing to know which policies we created, rather than just which policies have an automation link back to an installer, as there are more use cases where the reasonable admin flow involves us modifying/deleting policies on the admin's behalf.

iansltx avatar Feb 19 '25 20:02 iansltx

Also, given that automatic_install is currently a boolean flag on the API, we'll either need to break BC on that (albeit on endpoints marked as experimental) or keep the flag's current behavior (install this if it's not installed at all) and add more API surface for more tunable behavior. Which, if we have the two checkboxes in the above comment as how we show the UX we can add automatic_update to automatic_install, with both of those as booleans, and we get a BC API change.

iansltx avatar Feb 19 '25 20:02 iansltx

Additionally, as of right now the installer version is only parsed when the installer is uploaded, and the installer is only uploaded when the form is submitted, for custom packages anyway, so we can't autofill a version number in the minimum version.

iansltx avatar Feb 19 '25 20:02 iansltx

Hi all. I've mocked up two prototypes to discuss tomorrow. One follows the path we were on, the other incoporates @RachelElysia and @iansltx's comments.

  1. Option 1
  2. Option 2

eugkuo avatar Feb 19 '25 22:02 eugkuo

Comments from 21 Feb 2025 review:

  • Need to add rrror messages for server side validation:
    • All software (Fleet-maintained, App Store, custom packages)
    • For a version that won’t work
    • For a version that we know is greater than the app’s version. Which causes the infinite install loop.
  • Render custom and App Store app screens
    • Note: No autopopulate version for custom packages because we can’t
  • Add front end validation for missing minimum version
  • Consider some kind of illustration for self-service? How can we make this page more exciting? And scannable
  • Wordsmithing

eugkuo avatar Feb 20 '25 19:02 eugkuo

Here are updates based on today's comments:

eugkuo avatar Feb 21 '25 07:02 eugkuo

Notes from 22 Feb 2025 review:

  1. Noah: Add example of policy name
    1. What naming convention (name, description) do we follow for automatically created policies for update. We can follow pattern for we have “install”
  2. Noah: Add client side error for wrong version. Follow existing UI patterns we have;
    1. Had conversation in standup with Janis and Ian. Will not be doing client side validation. Validation will happen server side.
    2. Add error message after server side check. Something like: Min version specified is not going to work (won’t be able to compare it correctly
  3. Noah: Add client-side error if not using semver. So no “23”
    1. Check https://fleetdm.com/storybook/?path=/docs/components-formfields-slider--docs for

eugkuo avatar Feb 21 '25 17:02 eugkuo

@iansltx Speaking of version validation and moving it to server-side, do you think would we want to do client-side for 3, above where someone enters 23? OR do you think we should let 23 go through and automagically assume it's 23.0.0 (or should we do something else)?

eugkuo avatar Feb 21 '25 18:02 eugkuo

IMO no client-side validation whatsoever and leave any transformations to server-side based on what we find out about version_compare. There are some version formats that have a leading (colon-separated) epoch separator that osquery says it knows how to handle when you specify "arch" as the version comparison flavor, as an example. More info here.

To raise a flag, I'm seeing problematic results from osquery on simple queries (e.g. `SELECT VERSION_COMPARE('23.0.0', '23.0.0.0'), like comparing versions that should be equivalent: 23 vs. 23.0 vs. 23.0.0 should all be equal but for osquery 23 < 23.0 < 23.0.0. There's e.g. https://github.com/osquery/osquery/pull/6535 to fix that but it looks like attempts to fix those comparisons has gone nowhere. So there's a good chance that, to get these comparisons working properly via our standard policy framework, we'll have to patch osquery.

Maybe we can include a better version comparison function in not-C++ that we could deliver with an updated fleetd but not sure what that would entail.

EDIT: I'm wrong on desired functionality on 23.0.0 vs. 23.0.0.0; just checked e.g. PHP's version_compare function and it behaves the same way (less specific evaluates as less than more specific even if more specific has all zeroes in the new positions).

iansltx avatar Feb 21 '25 18:02 iansltx

Also, there are a bunch of apps (including several FMAs, e.g. Chrome, 1Password, Cloudflare Warp, Teams, WhatsApp, Zoom) that have a four-segment version number, so enforcing major.minor.patch for any end user minimum would break those.

Given that complexity, feels like the first iteration of this should be "get this to work with FMAs", and once we know we're reliably handling those we can use those learnings to tackle the wider world of arbitrary installers.

iansltx avatar Feb 21 '25 19:02 iansltx

Thinking through the above a little more, 23 < 23.0 < 23.0.0 doesn't actually hurt us for minimum version queries as long as compare the result of the comparison (-1, 0, 1) correctly. Min version of 23 would pass for 23.0 just like it would for 23.1, which is what we want.

So this comes down to confirming that for Win/Mac we don't get reported non-semver versions (x.y.z.a is semver-compliant for our purposes) in osquery, and that rhel/deb Linuxes mesh well with those flavors of osquery VERSION_COMPARE().

Still thinking this is much safer to target for either just FMA or FMA+VPP with custom packages in iteration 2. VPP is probably fine as Apple says its version struct (for Swift at least) needs to be semver-compatible for each component.

iansltx avatar Feb 21 '25 19:02 iansltx

@noahtalerman @eugkuo Sorry for the lack of clarity on the previous comments.

Responding to most recent design review discussions, forcing x.y.z.a for all packages, including ones where the installed version is x, x.y, or x.y.z, actually breaks for the "current version is installed" scenario, while allowing less-specific minimum versions will work fine specifically for the minimum version case. To illustrate:

Installed Version Min Version VERSION_COMPARE(installed, minimum) "Is Up to date" policy passes
2 2 equal to ✔️
2.0 2 greater than ✔️
2.0.0 2 greater than ✔️
2.0.0 2.0 greater than ✔️
2.0.0 2.0.0 equal to ✔️
2.0.0 2.0.0.0 less than
2.0 2.0.0.0 less than
2 2.0.0.0 less than

The actual return value is -1 (less than), 0 (equal to), or 1 (greater than) but I figured spelling things out makes the table more legible.

You can see this in action by running this query:

SELECT bundle_short_version,
version_compare(bundle_short_version, '24'),
version_compare(bundle_short_version, '24.005'),
version_compare(bundle_short_version, '24.005.20400'),
version_compare(bundle_short_version, '24.005.20400.0')
FROM apps WHERE bundle_identifier = 'com.adobe.Reader';

As I'm typing this, the latest version of Acrobat Reader appears to be 24.005.20400, so specifying 24.005.20400.0 in the auto-install policy would cause an infinite install loop, while specifying 24 would pass the policy as even 24.0.0 would evaluate as greater than that, which is sufficient to make the policy pass.

All of the above examples use the default (semver) mode on osquery's VERSION_COMPARE() function, without modifying anything about the version string on the way in. I wasn't suggesting zero-filling minimum version numbers server-side or the like, partly because as shown above zero-filling causes problems no matter who does it.

Hope the examples are helpful here!

iansltx avatar Feb 22 '25 06:02 iansltx

@RachelElysia We talked this morning about the min version head in this being bold as it looks like a form field head. However this going to be a radio button once we add 'latest'. I'm not sure what's more efficient for you (whether to make this a 'masked' radio button now or to make it a form header and then change it later?

Screenshot 2025-02-25 at 02.46.21.png Screenshot 2025-02-25 at 02.47.16.png

eugkuo avatar Feb 24 '25 17:02 eugkuo

@eugkuo if we're doing 2 iterations, we might as well have a more specific checkbox label in V1 and then have V2 checkbox label be more encompassing both options? It'll only take a few minutes to change

Image

RachelElysia avatar Feb 24 '25 18:02 RachelElysia

Should we have the words Automation/Automatic/ Auto in this? Like Auto-install or Auto-update? Or whatever automatic wording we're using on the software details page to indicate these features?

RachelElysia avatar Feb 24 '25 18:02 RachelElysia

Should we have the words Automation/Automatic/ Auto in this? Like Auto-install or Auto-update? Or whatever automatic wording we're using on the software details page to indicate these features?

@RachelElysia I feel like this is implied?

eugkuo avatar Feb 25 '25 02:02 eugkuo

Notes from 25 Feb 2025 review:

  1. TODO: Eugene: Dev note for staying on the Add software page when there’s an error
  2. TODO: Eugene: Description for update policies
  3. TODO: Eugene: What do we show on the software title page if “update” is selected
    1. Noah: Maybe we call this “Actions”? To make things consistent with the “Add software” flow

eugkuo avatar Feb 25 '25 02:02 eugkuo

A few updates here:

  1. In the main flow, updated 'Update' to "Update from version'. After chatting with @RachelElysia (and given that we're moving to radio buttons for this in the future) it seemed like it would be better not to use the form field component here in favor of something 'simpler' that could be inhereted by the radio buttons in future.
  2. Removed tooltips from action selections.
  3. Updated semantic error message
  4. Updated flash message so that it's rendered on the add software page to better indicate where it should appear.
  5. For the software detail page, there's a future design where we surface the policies. We could pull that forward, which would obviate the need for the 'auto-install' pill, which we'd get rid of then anyway. Thoughts?
  6. Added a policy detail page to show 'Upgrade'. I've hacked this together for now b/c I was having difficulty finding a figma example of this. I've left the query field blank for now as I think dev will fill this in?

FYI @noahtalerman

eugkuo avatar Feb 25 '25 03:02 eugkuo

Yeah, we can fill the query field in based on what the query actually needs to be. All of that page will basically be "what data do we populate" rather than actual UI changes.

Re: pulling the policies on the software title page forward, I'm personally in favor and don't think it would be a massive FE lift. We're already delivering that info in the API response that lowers that page, so that would require zero backend effort to do.

iansltx avatar Feb 25 '25 03:02 iansltx

@eugkuo On Friday I promised to confirm whether the two "counts updated at" values on the software title details page could be condensed into one (due to either being the same underlying value or keying off of a value that was updated in the same cron).

Turns out, they're using the exact same value: counts_updated_at in the software title response. So it's 100% acceptable to get rid of the duplication, now that versions and the summary information are in the same place rather than having softwre package details sandwiched between.

iansltx avatar Mar 03 '25 05:03 iansltx

@eugkuo On Friday I promised to confirm whether the two "counts updated at" values on the software title details page could be condensed into one (due to either being the same underlying value or keying off of a value that was updated in the same cron).

Turns out, they're using the exact same value: counts_updated_at in the software title response. So it's 100% acceptable to get rid of the duplication, now that versions and the summary information are in the same place rather than having softwre package details sandwiched between.

W00t! I've moved the timestamp so that it's next to the software name

@iansltx

cc @noahtalerman

eugkuo avatar Mar 03 '25 06:03 eugkuo

I've added a figma file that shows proposed YAML changes to support this ticket.

@noahtalerman @iansltx This should be building off of what we already had. LMK what you think.

eugkuo avatar Mar 03 '25 06:03 eugkuo

@eugkuo Couple of tweaks/questions on the YAML side:

version -> min_version

Going back to I think an older edition of this for clarity. The other option here would be to use version: ">= 1.2.3" but in order to be valid YAML we need the quotes, so y'all's call on whether forcing quoting is too ugly and we should use min_version instead. We could always add version later with the ability to do fancier constraints (e.g. the carat one for apps that shouldn't be unilaterally bumped across major versions) later.

Nesting

Thinking that more detailed ensure directives should be nested under ensure rather than at the same level. Catch is we'll need a new key to associate with present or updated because you can't nest under e.g. ensure: present.

Maybe something like this (existing key name isn't great):

software:
  packages:
    - path: ../lib/software-name2.package.yml
      ensure:
        on_host: updated
        min_version: 2.0.1
  app_store_apps:
    - app_store_id: "1091189122"
      ensure: present
  fleet_maintained_apps:
    - fleet_maintained_app_id: slack/darwin
      ensure:
        on_host: present
        min_version: 2.0.1

FMA slug reverse

See https://github.com/fleetdm/fleet/issues/26080#issuecomment-2686917593 for rationale. Reflected in the FMA YAML above. tl;dr: since we're grouping multiple platforms into the same FMA this supports that use case more cleanly. Reflected in the above YAML smippet.

It's worthwhile even though that means apps from different sources (Homebrew, WinGet) will wind up being dumped into the same slug prefix and FMA manifest outputs subdirectory.

There are other thorny edge cases around FMAs in YAML that we'll need to sort out but that's its own ticket.

ensure: present vs. ensure: updated

If you wanted the equivalent of "check both boxes", you'd do ensure: present and also specify a minimum version, right? ensure: updated would be the eqivalent to checking just the bottom box, and ensure: present with no minimum version would be equivalent to checking just the top box. That sound right?

iansltx avatar Mar 03 '25 06:03 iansltx

@iansltx Ah you're right. We have two operators. I like the idea of nesting, but I'm not sure why we have to use 'on-host'? I'm also ok with using quotatation marks in YAML since we're already doing it for things like app id. And I'd rather use version with fun operators now than create specific operators that we'd have to continue to maintain in the future after we do the fancier version thing.

I've updated the figma. LMK what you think.

cc @noahtalerman

eugkuo avatar Mar 03 '25 06:03 eugkuo

So, that isn't valid YAML. Can't duplicate string keys, and can't declare both a single line value and child values. https://www.yamllint.com/ is helpful for this.

iansltx avatar Mar 03 '25 06:03 iansltx

@iansltx Ah I see. I updated the figma file like so.

eugkuo avatar Mar 03 '25 06:03 eugkuo

Missed a spot in Figma for packages. Probably good to show "updated" there as an example of when we only want to touch titles that are already on the host.

Hopefully we can come up with a better key name than on_host BTW. It's sufficiently descriptive but a little clunky. Can talk through in an hour.

iansltx avatar Mar 03 '25 14:03 iansltx

@iansltx There are no API changes for this, yes?

eugkuo avatar Mar 03 '25 17:03 eugkuo