mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Android software ingestion (#33826)
> Closes #33581 <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves # # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked table schema to confirm autoupdate - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). --------- Co-authored-by: RachelElysia <rachel@fleetdm.com>
This commit is contained in:
parent
e01f8c7ebd
commit
0a3c6c35d3
25 changed files with 762 additions and 148 deletions
1
changes/33581-android-sw-ingestion
Normal file
1
changes/33581-android-sw-ingestion
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Adds support for software inventory on Android hosts.
|
||||
|
|
@ -785,6 +785,7 @@ the way that the Fleet server works.
|
|||
svc,
|
||||
config.License.Key,
|
||||
config.Server.PrivateKey,
|
||||
ds,
|
||||
)
|
||||
if err != nil {
|
||||
initFatal(err, "initializing android service")
|
||||
|
|
|
|||
|
|
@ -128,8 +128,7 @@ const HostSoftware = ({
|
|||
}: IHostSoftwareProps) => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
|
||||
const isUnsupported =
|
||||
isAndroid(platform) || (isIPadOrIPhone(platform) && queryParams.vulnerable); // no Android software and no vulnerable software for iOS
|
||||
const isUnsupported = isIPadOrIPhone(platform) && queryParams.vulnerable; // no Android software and no vulnerable software for iOS
|
||||
|
||||
const [showSoftwareFiltersModal, setShowSoftwareFiltersModal] = useState(
|
||||
false
|
||||
|
|
|
|||
|
|
@ -47,17 +47,6 @@ describe("HostSoftwareTable", () => {
|
|||
expect(screen.getByText(/no software detected/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the Android not supported state", () => {
|
||||
renderWithContext({ platform: "android" });
|
||||
expect(
|
||||
screen.getByText(/software is not supported for this host/i)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/let us know/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Software installed on this host/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders custom filter button when filters are applied", () => {
|
||||
renderWithContext({
|
||||
vulnFilters: { vulnerable: true },
|
||||
|
|
|
|||
|
|
@ -178,20 +178,6 @@ const HostSoftwareTable = ({
|
|||
[onShowInventoryVersions]
|
||||
);
|
||||
|
||||
if (isAndroid(platform)) {
|
||||
return (
|
||||
<EmptyTable
|
||||
header="Software is not supported for this host"
|
||||
info={
|
||||
<>
|
||||
Interested in viewing software for Android hosts?{" "}
|
||||
<CustomLink url={SUPPORT_LINK} text="Let us know" newTab />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderCustomFiltersButton = () => {
|
||||
return (
|
||||
<TooltipWrapper
|
||||
|
|
|
|||
54
go.mod
54
go.mod
|
|
@ -3,7 +3,7 @@ module github.com/fleetdm/fleet/v4
|
|||
go 1.25.1
|
||||
|
||||
require (
|
||||
cloud.google.com/go/pubsub v1.45.1
|
||||
cloud.google.com/go/pubsub v1.49.0
|
||||
fyne.io/systray v1.10.1-0.20240111184411-11c585fff98d
|
||||
github.com/AbGuthrie/goquery/v2 v2.0.1
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
||||
|
|
@ -142,25 +142,25 @@ require (
|
|||
go.elastic.co/apm/v2 v2.7.0
|
||||
go.etcd.io/bbolt v1.3.10
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.60.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
|
||||
go.opentelemetry.io/otel/sdk v1.37.0
|
||||
go.opentelemetry.io/otel/trace v1.37.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/mod v0.25.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/mod v0.26.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/text v0.26.0
|
||||
golang.org/x/tools v0.33.0
|
||||
google.golang.org/api v0.215.0
|
||||
google.golang.org/grpc v1.73.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/term v0.34.0
|
||||
golang.org/x/text v0.28.0
|
||||
golang.org/x/tools v0.35.0
|
||||
google.golang.org/api v0.249.0
|
||||
google.golang.org/grpc v1.75.0
|
||||
gopkg.in/guregu/null.v3 v3.5.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
|
|
@ -171,11 +171,11 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/iam v1.2.2 // indirect
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
|
|
@ -248,15 +248,15 @@ require (
|
|||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v1.2.4 // indirect
|
||||
github.com/golang/glog v1.2.5 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm-tools v0.4.5 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/goreleaser/chglog v0.4.2 // indirect
|
||||
github.com/goreleaser/fileglob v1.3.0 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
|
|
@ -333,15 +333,15 @@ require (
|
|||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
|
|
|||
126
go.sum
126
go.sum
|
|
@ -1,20 +1,20 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
|
||||
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
|
||||
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
|
||||
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
|
||||
cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
|
||||
cloud.google.com/go/kms v1.20.1 h1:og29Wv59uf2FVaZlesaiDAqHFzHaoUyHI3HYp9VUHVg=
|
||||
cloud.google.com/go/kms v1.20.1/go.mod h1:LywpNiVCvzYNJWS9JUcGJSVTNSwPwi0vBAotzDqn2nc=
|
||||
cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
|
||||
cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
|
||||
cloud.google.com/go/pubsub v1.45.1 h1:ZC/UzYcrmK12THWn1P72z+Pnp2vu/zCZRXyhAfP1hJY=
|
||||
cloud.google.com/go/pubsub v1.45.1/go.mod h1:3bn7fTmzZFwaUjllitv1WlsNMkqBgGUb3UdMhI54eCc=
|
||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
|
||||
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
|
||||
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
|
||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=
|
||||
cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
cloud.google.com/go/pubsub v1.49.0 h1:5054IkbslnrMCgA2MAEPcsN3Ky+AyMpEZcii/DoySPo=
|
||||
cloud.google.com/go/pubsub v1.49.0/go.mod h1:K1FswTWP+C1tI/nfi3HQecoVeFvL4HUOB1tdaNXKhUY=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
|
|
@ -392,8 +392,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe
|
|||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
|
||||
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
|
||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
|
|
@ -459,17 +459,17 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ=
|
||||
github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/goreleaser/chglog v0.4.2 h1:afmbT1d7lX/q+GF8wv3a1Dofs2j/Y9YkiCpGemWR6mI=
|
||||
|
|
@ -883,8 +883,8 @@ github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
|||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
|
||||
go.einride.tech/aip v0.68.0 h1:4seM66oLzTpz50u4K1zlJyOXQ3tCzcJN7I22tKkjipw=
|
||||
go.einride.tech/aip v0.68.0/go.mod h1:7y9FF8VtPWqpxuAxl0KQWqaULxW4zFIesD6zF5RIHHg=
|
||||
go.einride.tech/aip v0.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ=
|
||||
go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg=
|
||||
go.elastic.co/apm/module/apmgorilla/v2 v2.6.2 h1:/myBx0D/JiwTUjFkVFG3zXmDfGPfQjP/cg27qcBbdfU=
|
||||
go.elastic.co/apm/module/apmgorilla/v2 v2.6.2/go.mod h1:uONZzSIh/cKjQ2rZmINR1VXVOJDq5eWOzKrCY+bu00w=
|
||||
go.elastic.co/apm/module/apmhttp/v2 v2.7.1-0.20250407084155-22ab1be21948 h1:FS1GGVsZoIxezIGL2N3ExjQJzBA3Ne9hxp6HKvUhcRo=
|
||||
|
|
@ -906,10 +906,10 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
|
|||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.60.0 h1:iLuogsToNW6QaOYPcbIwhkdRTkc0gvXzuiajObXc6WY=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.60.0/go.mod h1:XNSNQBtSOifFUw0aQUyBN0Ff+0NddEnbSATy2QlFgm8=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
|
|
@ -922,8 +922,8 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd
|
|||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
|
|
@ -942,8 +942,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
|
|
@ -956,8 +956,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
@ -980,8 +980,8 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
|
|
@ -993,8 +993,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -1040,14 +1040,14 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
|
@ -1055,10 +1055,10 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
|
@ -1071,14 +1071,16 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
|||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0=
|
||||
google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w=
|
||||
google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
|
|
@ -1086,20 +1088,20 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
|
|||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
|
@ -1111,8 +1113,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
|||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20251003094629, Down_20251003094629)
|
||||
}
|
||||
|
||||
func Up_20251003094629(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`ALTER TABLE software_titles ADD COLUMN application_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add software_titles.application_id column: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`ALTER TABLE software ADD COLUMN application_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add software.application_id column: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE software_titles
|
||||
DROP INDEX idx_unique_sw_titles
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to drop unique index: %w", err)
|
||||
}
|
||||
|
||||
// Drop the column to update the definition
|
||||
_, err = tx.Exec(`ALTER TABLE software_titles DROP COLUMN unique_identifier`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to drop software_titles.unique_identifier column: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
ALTER TABLE software_titles
|
||||
ADD COLUMN unique_identifier VARCHAR(255) GENERATED ALWAYS AS (COALESCE(bundle_identifier, application_id, name)) VIRTUAL;
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add generated column unique_identifier: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE software_titles
|
||||
ADD UNIQUE INDEX idx_unique_sw_titles (unique_identifier, source, extension_for);
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add unique index: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20251003094629(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tj/assert"
|
||||
)
|
||||
|
||||
func TestUp_20251003094629(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// Add some non-Android software. The unique_identifier should be the bundle_identifier for the macOS software and the name for the Windows software.
|
||||
stIDMac := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, bundle_identifier) VALUES ("iTerm.app", "apps", "com.googlecode.iterm2")`)
|
||||
stIDWindows := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source) VALUES ("Notepad", "programs")`)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// Now that the application_id column exists, we can add some Android software.
|
||||
stIDAndroid := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, application_id) VALUES ("YouTube", "android_apps", "com.google.youtube")`)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
titleID int64
|
||||
expectedBundleID *string
|
||||
expectedApplicationID *string
|
||||
expectedUniqueIdentifier string
|
||||
}{
|
||||
{
|
||||
name: "macOS software title",
|
||||
titleID: stIDMac,
|
||||
expectedBundleID: ptr.String("com.googlecode.iterm2"),
|
||||
expectedUniqueIdentifier: "com.googlecode.iterm2",
|
||||
},
|
||||
{
|
||||
name: "android software title",
|
||||
titleID: stIDAndroid,
|
||||
expectedApplicationID: ptr.String("com.google.youtube"),
|
||||
expectedUniqueIdentifier: "com.google.youtube",
|
||||
},
|
||||
|
||||
{
|
||||
name: "windows software title",
|
||||
titleID: stIDWindows,
|
||||
expectedUniqueIdentifier: "Notepad",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var title fleet.SoftwareTitle
|
||||
err := db.Get(&title, "SELECT id, name, source, extension_for, application_id, bundle_identifier FROM software_titles WHERE id = ?", tt.titleID)
|
||||
require.NoError(t, err)
|
||||
switch {
|
||||
case tt.expectedBundleID == nil:
|
||||
require.Nil(t, title.BundleIdentifier)
|
||||
|
||||
case tt.expectedBundleID != nil:
|
||||
require.NotNil(t, tt.expectedBundleID)
|
||||
assert.Equal(t, *tt.expectedBundleID, *title.BundleIdentifier)
|
||||
|
||||
case tt.expectedApplicationID == nil:
|
||||
require.Nil(t, title.ApplicationID)
|
||||
|
||||
case tt.expectedApplicationID != nil:
|
||||
require.NotNil(t, title.ApplicationID)
|
||||
assert.Equal(t, tt.expectedApplicationID, title.ApplicationID)
|
||||
|
||||
}
|
||||
|
||||
var gotUniqueID string
|
||||
err = db.Get(&gotUniqueID, "SELECT unique_identifier FROM software_titles WHERE id = ?", tt.titleID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.expectedUniqueIdentifier, gotUniqueID)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -964,6 +964,9 @@ func (ds *Datastore) preInsertSoftwareInventory(
|
|||
if sw.BundleIdentifier != "" {
|
||||
st.BundleIdentifier = ptr.String(sw.BundleIdentifier)
|
||||
}
|
||||
if sw.ApplicationID != nil && *sw.ApplicationID != "" {
|
||||
st.ApplicationID = sw.ApplicationID
|
||||
}
|
||||
newTitlesNeeded[checksum] = st
|
||||
}
|
||||
}
|
||||
|
|
@ -1003,12 +1006,12 @@ func (ds *Datastore) preInsertSoftwareInventory(
|
|||
|
||||
// Insert software titles
|
||||
const numberOfArgsPerSoftwareTitles = 5
|
||||
titlesValues := strings.TrimSuffix(strings.Repeat("(?,?,?,?,?),", len(uniqueTitlesToInsert)), ",")
|
||||
titlesStmt := fmt.Sprintf("INSERT IGNORE INTO software_titles (name, source, extension_for, bundle_identifier, is_kernel) VALUES %s", titlesValues)
|
||||
titlesValues := strings.TrimSuffix(strings.Repeat("(?,?,?,?,?,?),", len(uniqueTitlesToInsert)), ",")
|
||||
titlesStmt := fmt.Sprintf("INSERT IGNORE INTO software_titles (name, source, extension_for, bundle_identifier, is_kernel, application_id) VALUES %s", titlesValues)
|
||||
titlesArgs := make([]any, 0, len(uniqueTitlesToInsert)*numberOfArgsPerSoftwareTitles)
|
||||
|
||||
for _, title := range uniqueTitlesToInsert {
|
||||
titlesArgs = append(titlesArgs, title.Name, title.Source, title.ExtensionFor, title.BundleIdentifier, title.IsKernel)
|
||||
titlesArgs = append(titlesArgs, title.Name, title.Source, title.ExtensionFor, title.BundleIdentifier, title.IsKernel, title.ApplicationID)
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, titlesStmt, titlesArgs...); err != nil {
|
||||
|
|
@ -1066,7 +1069,7 @@ func (ds *Datastore) preInsertSoftwareInventory(
|
|||
// Insert software entries
|
||||
const numberOfArgsPerSoftware = 11
|
||||
values := strings.TrimSuffix(
|
||||
strings.Repeat("(?,?,?,?,?,?,?,?,?,?,?),", len(batchKeys)), ",",
|
||||
strings.Repeat("(?,?,?,?,?,?,?,?,?,?,?,?),", len(batchKeys)), ",",
|
||||
)
|
||||
stmt := fmt.Sprintf(
|
||||
`INSERT IGNORE INTO software (
|
||||
|
|
@ -1080,7 +1083,8 @@ func (ds *Datastore) preInsertSoftwareInventory(
|
|||
extension_id,
|
||||
extension_for,
|
||||
title_id,
|
||||
checksum
|
||||
checksum,
|
||||
application_id
|
||||
) VALUES %s`,
|
||||
values,
|
||||
)
|
||||
|
|
@ -1100,7 +1104,7 @@ func (ds *Datastore) preInsertSoftwareInventory(
|
|||
}
|
||||
args = append(
|
||||
args, sw.Name, sw.Version, sw.Source, sw.Release, sw.Vendor, sw.Arch,
|
||||
sw.BundleIdentifier, sw.ExtensionID, sw.ExtensionFor, titleID, checksum,
|
||||
sw.BundleIdentifier, sw.ExtensionID, sw.ExtensionFor, titleID, checksum, sw.ApplicationID,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1478,6 +1482,7 @@ func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, e
|
|||
"s.release",
|
||||
"s.vendor",
|
||||
"s.arch",
|
||||
"s.application_id",
|
||||
goqu.I("scp.cpe").As("generated_cpe"),
|
||||
).
|
||||
// Include this in the sub-query in case we want to sort by 'generated_cpe'
|
||||
|
|
@ -1658,6 +1663,7 @@ func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, e
|
|||
"s.release",
|
||||
"s.vendor",
|
||||
"s.arch",
|
||||
"s.application_id",
|
||||
goqu.COALESCE(goqu.I("s.generated_cpe"), "").As("generated_cpe"),
|
||||
"scv.cve",
|
||||
"scv.created_at",
|
||||
|
|
@ -4766,7 +4772,7 @@ func (ds *Datastore) SetHostSoftwareInstallResult(ctx context.Context, result *f
|
|||
func (ds *Datastore) CreateIntermediateInstallFailureRecord(ctx context.Context, result *fleet.HostSoftwareInstallResultPayload) (string, *fleet.HostSoftwareInstallerResult, bool, error) {
|
||||
// Get the original installation details first, including software title and package info
|
||||
const getDetailsStmt = `
|
||||
SELECT
|
||||
SELECT
|
||||
hsi.software_installer_id,
|
||||
hsi.user_id,
|
||||
hsi.policy_id,
|
||||
|
|
|
|||
|
|
@ -3567,6 +3567,7 @@ func testVerifySoftwareChecksum(t *testing.T, ds *Datastore) {
|
|||
{Name: "foo", Version: "0.0.1", Source: "test", ExtensionFor: "firefox"},
|
||||
{Name: "foo", Version: "0.0.1", Source: "test", ExtensionID: "ext"},
|
||||
{Name: "foo", Version: "0.0.2", Source: "test"},
|
||||
{Name: "foo", Version: "0.0.2", Source: "test", ApplicationID: ptr.String("foo.bar.baz")},
|
||||
}
|
||||
|
||||
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
||||
|
|
@ -3582,7 +3583,7 @@ func testVerifySoftwareChecksum(t *testing.T, ds *Datastore) {
|
|||
var got fleet.Software
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &got,
|
||||
`SELECT name, version, source, bundle_identifier, `+"`release`"+`, arch, vendor, extension_for, extension_id FROM software WHERE checksum = UNHEX(?)`, cs)
|
||||
`SELECT name, version, source, bundle_identifier, `+"`release`"+`, arch, vendor, extension_for, extension_id, application_id FROM software WHERE checksum = UNHEX(?)`, cs)
|
||||
})
|
||||
require.Equal(t, software[i], got)
|
||||
}
|
||||
|
|
@ -5641,7 +5642,7 @@ func testCreateIntermediateInstallFailureRecord(t *testing.T, ds *Datastore) {
|
|||
originalCreatedAt := time.Now().Add(-1 * time.Hour).UTC().Truncate(time.Microsecond) // Set to 1 hour ago
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `
|
||||
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, user_id, policy_id, self_service, created_at)
|
||||
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, user_id, policy_id, self_service, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
"original-uuid", host.ID, installerID, user.ID, nil, false, originalCreatedAt)
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ SELECT
|
|||
st.source,
|
||||
st.extension_for,
|
||||
st.bundle_identifier,
|
||||
st.application_id,
|
||||
COALESCE(sthc.hosts_count, 0) AS hosts_count,
|
||||
MAX(sthc.updated_at) AS counts_updated_at,
|
||||
COUNT(si.id) as software_installers_count,
|
||||
|
|
@ -364,6 +365,7 @@ SELECT
|
|||
,st.source
|
||||
,st.extension_for
|
||||
,st.bundle_identifier
|
||||
,st.application_id
|
||||
,MAX(COALESCE(sthc.hosts_count, 0)) as hosts_count
|
||||
,MAX(COALESCE(sthc.updated_at, date('0001-01-01 00:00:00'))) as counts_updated_at
|
||||
{{if hasTeamID .}}
|
||||
|
|
@ -387,10 +389,10 @@ FROM software_titles st
|
|||
{{$installerJoin := printf "%s JOIN software_installers si ON si.title_id = st.id AND si.global_or_team_id = %d" (yesNo .PackagesOnly "INNER" "LEFT") (teamID .)}}
|
||||
{{$installerJoin}}
|
||||
LEFT JOIN vpp_apps vap ON vap.title_id = st.id AND {{yesNo .PackagesOnly "FALSE" "TRUE"}}
|
||||
LEFT JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform AND
|
||||
LEFT JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform AND
|
||||
{{if .PackagesOnly}} FALSE {{else}} vat.global_or_team_id = {{teamID .}}{{end}}
|
||||
{{end}}
|
||||
LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND
|
||||
LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND
|
||||
(sthc.team_id = {{teamID .}} AND sthc.global_stats = {{if hasTeamID .}} 0 {{else}} 1 {{end}})
|
||||
{{with $softwareJoin := " "}}
|
||||
{{if or $.ListOptions.MatchQuery $.VulnerableOnly}}
|
||||
|
|
@ -415,7 +417,7 @@ FROM software_titles st
|
|||
{{end}}
|
||||
{{$softwareJoin}}
|
||||
{{end}}
|
||||
WHERE
|
||||
WHERE
|
||||
{{with $additionalWhere := "TRUE"}}
|
||||
{{if $.ListOptions.MatchQuery}}
|
||||
{{$additionalWhere = "(st.name LIKE ? OR scve.cve LIKE ?)"}}
|
||||
|
|
@ -437,7 +439,7 @@ WHERE
|
|||
{{end}}
|
||||
AND ({{$defFilter}})
|
||||
{{end}}
|
||||
GROUP BY
|
||||
GROUP BY
|
||||
st.id
|
||||
{{if hasTeamID .}}
|
||||
,package_self_service
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -2438,6 +2438,12 @@ type AndroidDatastore interface {
|
|||
// ListHostMDMAndroidProfilesPendingInstallWithVersion returns a list of all android profiles that are pending install, and where version is less than or equals to the policyVersion.
|
||||
ListHostMDMAndroidProfilesPendingInstallWithVersion(ctx context.Context, hostUUID string, policyVersion int64) ([]*MDMAndroidProfilePayload, error)
|
||||
GetAndroidPolicyRequestByUUID(ctx context.Context, requestUUID string) (*MDMAndroidPolicyRequest, error)
|
||||
// UpdateHostSoftware updates the software list of a host.
|
||||
// The update consists of deleting existing entries that are not in the given `software`
|
||||
// slice, updating existing entries and inserting new entries.
|
||||
// Returns a struct with the current installed software on the host (pre-mutations) plus all
|
||||
// mutations performed: what was inserted and what was removed.
|
||||
UpdateHostSoftware(ctx context.Context, hostID uint, software []Software) (*UpdateHostSoftwareDBResult, error)
|
||||
}
|
||||
|
||||
// MDMAppleStore wraps nanomdm's storage and adds methods to deal with
|
||||
|
|
|
|||
|
|
@ -96,7 +96,10 @@ type Software struct {
|
|||
// TODO: should we create a separate type? Feels like this field shouldn't be here since it's
|
||||
// just used for VPP install verification.
|
||||
Installed bool `json:"-"`
|
||||
IsKernel bool `json:"-"`
|
||||
// IsKernel indicates if this software is a Linux kernel.
|
||||
IsKernel bool `json:"-"`
|
||||
// ApplicationID is the unique identifier for Android software. Equivalent to the BundleIdentifier on Apple software.
|
||||
ApplicationID *string `json:"application_id,omitempty" db:"application_id"`
|
||||
}
|
||||
|
||||
func (Software) AuthzType() string {
|
||||
|
|
@ -136,6 +139,9 @@ func (s Software) ToUniqueStr() string {
|
|||
if s.ExtensionID != "" || s.ExtensionFor != "" {
|
||||
ss = append(ss, s.ExtensionID, s.ExtensionFor)
|
||||
}
|
||||
if s.ApplicationID != nil && *s.ApplicationID != "" {
|
||||
ss = append(ss, *s.ApplicationID)
|
||||
}
|
||||
return strings.Join(ss, SoftwareFieldSeparator)
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +155,9 @@ func (s Software) ComputeRawChecksum() ([]byte, error) {
|
|||
if s.Source != "apps" {
|
||||
cols = append([]string{s.Name}, cols...)
|
||||
}
|
||||
if s.ApplicationID != nil && *s.ApplicationID != "" {
|
||||
cols = append(cols, *s.ApplicationID)
|
||||
}
|
||||
_, err := fmt.Fprint(h, strings.Join(cols, "\x00"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -238,6 +247,8 @@ type SoftwareTitle struct {
|
|||
BundleIdentifier *string `json:"bundle_identifier,omitempty" db:"bundle_identifier"`
|
||||
// IsKernel indicates if the software title is a Linux kernel.
|
||||
IsKernel bool `json:"-" db:"is_kernel"`
|
||||
// ApplicationID is the unique identifier for Android software. Equivalent to the BundleIdentifier on Apple software.
|
||||
ApplicationID *string `json:"application_id,omitempty" db:"application_id"`
|
||||
}
|
||||
|
||||
// populateBrowserField populates the browser field for backwards compatibility
|
||||
|
|
@ -317,6 +328,8 @@ type SoftwareTitleListResult struct {
|
|||
// with existing software entries.
|
||||
BundleIdentifier *string `json:"bundle_identifier,omitempty" db:"bundle_identifier"`
|
||||
HashSHA256 *string `json:"hash_sha256,omitempty" db:"package_storage_id"`
|
||||
// ApplicationID is the unique identifier for Android software. Equivalent to the BundleIdentifier on Apple software.
|
||||
ApplicationID *string `json:"application_id,omitempty" db:"application_id"`
|
||||
}
|
||||
|
||||
type SoftwareTitleListOptions struct {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ type EnterprisesListFunc func(ctx context.Context, serverURL string) ([]*android
|
|||
|
||||
type SetAuthenticationSecretFunc func(secret string) error
|
||||
|
||||
type EnterprisesPoliciesModifyPolicyApplicationsFunc func(ctx context.Context, policyName string, policy *androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error)
|
||||
|
||||
type EnterprisesPoliciesRemovePolicyApplicationsFunc func(ctx context.Context, policyName string, appIDs []string) (*androidmanagement.Policy, error)
|
||||
|
||||
type EnterprisesApplicationsFunc func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error)
|
||||
|
||||
type Client struct {
|
||||
SignupURLsCreateFunc SignupURLsCreateFunc
|
||||
SignupURLsCreateFuncInvoked bool
|
||||
|
|
@ -64,6 +70,15 @@ type Client struct {
|
|||
SetAuthenticationSecretFunc SetAuthenticationSecretFunc
|
||||
SetAuthenticationSecretFuncInvoked bool
|
||||
|
||||
EnterprisesPoliciesModifyPolicyApplicationsFunc EnterprisesPoliciesModifyPolicyApplicationsFunc
|
||||
EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked bool
|
||||
|
||||
EnterprisesPoliciesRemovePolicyApplicationsFunc EnterprisesPoliciesRemovePolicyApplicationsFunc
|
||||
EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked bool
|
||||
|
||||
EnterprisesApplicationsFunc EnterprisesApplicationsFunc
|
||||
EnterprisesApplicationsFuncInvoked bool
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
|
|
@ -136,3 +151,24 @@ func (p *Client) SetAuthenticationSecret(secret string) error {
|
|||
p.mu.Unlock()
|
||||
return p.SetAuthenticationSecretFunc(secret)
|
||||
}
|
||||
|
||||
func (p *Client) EnterprisesPoliciesModifyPolicyApplications(ctx context.Context, policyName string, policy *androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error) {
|
||||
p.mu.Lock()
|
||||
p.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked = true
|
||||
p.mu.Unlock()
|
||||
return p.EnterprisesPoliciesModifyPolicyApplicationsFunc(ctx, policyName, policy)
|
||||
}
|
||||
|
||||
func (p *Client) EnterprisesPoliciesRemovePolicyApplications(ctx context.Context, policyName string, appIDs []string) (*androidmanagement.Policy, error) {
|
||||
p.mu.Lock()
|
||||
p.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked = true
|
||||
p.mu.Unlock()
|
||||
return p.EnterprisesPoliciesRemovePolicyApplicationsFunc(ctx, policyName, appIDs)
|
||||
}
|
||||
|
||||
func (p *Client) EnterprisesApplications(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
|
||||
p.mu.Lock()
|
||||
p.EnterprisesApplicationsFuncInvoked = true
|
||||
p.mu.Unlock()
|
||||
return p.EnterprisesApplicationsFunc(ctx, enterpriseName, packageName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func TestEnterprisesAuth(t *testing.T) {
|
|||
logger := kitlog.NewLogfmtLogger(os.Stdout)
|
||||
fleetDS := InitCommonDSMocks()
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
|
|
@ -128,7 +128,7 @@ func TestEnterpriseSignupMissingPrivateKey(t *testing.T) {
|
|||
fleetDS := InitCommonDSMocks()
|
||||
fleetSvc := mockService{}
|
||||
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
user := &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
|
|
@ -243,7 +243,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
|
||||
fleetDS := InitCommonDSMocks()
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
enterprise, err := svc.GetEnterprise(ctx)
|
||||
|
|
@ -280,7 +280,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
}
|
||||
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
enterprise, err := svc.GetEnterprise(ctx)
|
||||
|
|
@ -330,7 +330,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
}
|
||||
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
enterprise, err := svc.GetEnterprise(ctx)
|
||||
|
|
@ -372,7 +372,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
}
|
||||
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
enterprise, err := svc.GetEnterprise(ctx)
|
||||
|
|
@ -414,7 +414,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
}
|
||||
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
enterprise, err := svc.GetEnterprise(ctx)
|
||||
|
|
@ -482,7 +482,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
}
|
||||
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
enterprise, err := svc.GetEnterprise(ctx)
|
||||
|
|
@ -514,7 +514,7 @@ func TestGetEnterprise(t *testing.T) {
|
|||
}
|
||||
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
enterprise, err := svc.GetEnterprise(ctx)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ func attachFleetAPIRoutes(r *mux.Router, fleetSvc fleet.Service, svc android.Ser
|
|||
|
||||
ne.GET("/api/_version_/fleet/android_enterprise/connect/{token}", enterpriseSignupCallbackEndpoint, enterpriseSignupCallbackRequest{})
|
||||
ne.GET("/api/_version_/fleet/android_enterprise/enrollment_token", enrollmentTokenEndpoint, enrollmentTokenRequest{})
|
||||
ne.POST(pubSubPushPath, pubSubPushEndpoint, pubSubPushRequest{})
|
||||
ne.POST(pubSubPushPath, pubSubPushEndpoint, PubSubPushRequest{})
|
||||
}
|
||||
|
||||
func apiVersions() []string {
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ import (
|
|||
"google.golang.org/api/androidmanagement/v1"
|
||||
)
|
||||
|
||||
type pubSubPushRequest struct {
|
||||
type PubSubPushRequest struct {
|
||||
Token string `query:"token"`
|
||||
android.PubSubMessage `json:"message"`
|
||||
}
|
||||
|
||||
func pubSubPushEndpoint(ctx context.Context, request interface{}, svc android.Service) fleet.Errorer {
|
||||
req := request.(*pubSubPushRequest)
|
||||
req := request.(*PubSubPushRequest)
|
||||
err := svc.ProcessPubSubPush(ctx, req.Token, &req.PubSubMessage)
|
||||
return android.DefaultResponse{Err: err}
|
||||
}
|
||||
|
|
@ -181,6 +181,50 @@ func (svc *Service) handlePubSubStatusReport(ctx context.Context, token string,
|
|||
level.Debug(svc.logger).Log("msg", "Error updating Android host", "data", rawData)
|
||||
return ctxerr.Wrap(ctx, err, "enrolling Android host")
|
||||
}
|
||||
err = svc.updateHostSoftware(ctx, &device, host)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating Android host software")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// largely based on refetch apps code from Apple MDM service methods
|
||||
func (svc *Service) updateHostSoftware(ctx context.Context, device *androidmanagement.Device, host *fleet.AndroidHost) error {
|
||||
|
||||
// Do nothing if no app reports returned
|
||||
if len(device.ApplicationReports) == 0 {
|
||||
return nil
|
||||
}
|
||||
truncateString := func(item any, length int) string {
|
||||
str, ok := item.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(str)
|
||||
if len(runes) > length {
|
||||
return string(runes[:length])
|
||||
}
|
||||
return str
|
||||
}
|
||||
software := []fleet.Software{}
|
||||
for _, app := range device.ApplicationReports {
|
||||
if app.State != "INSTALLED" {
|
||||
continue
|
||||
}
|
||||
sw := fleet.Software{
|
||||
Name: truncateString(app.DisplayName, fleet.SoftwareNameMaxLength),
|
||||
Version: truncateString(app.VersionName, fleet.SoftwareVersionMaxLength),
|
||||
ApplicationID: ptr.String(truncateString(app.PackageName, fleet.SoftwareBundleIdentifierMaxLength)),
|
||||
Source: "android_apps",
|
||||
Installed: true,
|
||||
}
|
||||
software = append(software, sw)
|
||||
}
|
||||
|
||||
_, err := svc.fleetDS.UpdateHostSoftware(ctx, host.Host.ID, software)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating Android host software")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func createAndroidService(t *testing.T) (android.Service, *AndroidMockDS) {
|
|||
logger := kitlog.NewLogfmtLogger(os.Stdout)
|
||||
mockDS := InitCommonDSMocks()
|
||||
fleetSvc := mockService{}
|
||||
svc, err := NewServiceWithClient(logger, mockDS, &androidAPIClient, &fleetSvc, "test-private-key")
|
||||
svc, err := NewServiceWithClient(logger, mockDS, &androidAPIClient, &fleetSvc, "test-private-key", &mockDS.DataStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
return svc, mockDS
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ type Service struct {
|
|||
logger kitlog.Logger
|
||||
authz *authz.Authorizer
|
||||
ds fleet.AndroidDatastore
|
||||
fleetDS fleet.Datastore
|
||||
androidAPIClient androidmgmt.Client
|
||||
fleetSvc fleet.Service
|
||||
serverPrivateKey string
|
||||
|
|
@ -58,9 +59,10 @@ func NewService(
|
|||
fleetSvc fleet.Service,
|
||||
licenseKey string,
|
||||
serverPrivateKey string,
|
||||
fleetDS fleet.Datastore,
|
||||
) (android.Service, error) {
|
||||
client := newAMAPIClient(ctx, logger, licenseKey)
|
||||
return NewServiceWithClient(logger, ds, client, fleetSvc, serverPrivateKey)
|
||||
return NewServiceWithClient(logger, ds, client, fleetSvc, serverPrivateKey, fleetDS)
|
||||
}
|
||||
|
||||
func NewServiceWithClient(
|
||||
|
|
@ -69,6 +71,7 @@ func NewServiceWithClient(
|
|||
client androidmgmt.Client,
|
||||
fleetSvc fleet.Service,
|
||||
serverPrivateKey string,
|
||||
fleetDS fleet.Datastore,
|
||||
) (android.Service, error) {
|
||||
authorizer, err := authz.NewAuthorizer()
|
||||
if err != nil {
|
||||
|
|
@ -83,6 +86,7 @@ func NewServiceWithClient(
|
|||
fleetSvc: fleetSvc,
|
||||
serverPrivateKey: serverPrivateKey,
|
||||
SignupSSEInterval: DefaultSignupSSEInterval,
|
||||
fleetDS: fleetDS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
383
server/mdm/android/tests/integration_software_test.go
Normal file
383
server/mdm/android/tests/integration_software_test.go
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/android"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/android/service"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/go-json-experiment/json"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"google.golang.org/api/androidmanagement/v1"
|
||||
)
|
||||
|
||||
func TestServiceSoftware(t *testing.T) {
|
||||
testingSuite := new(softwareTestSuite)
|
||||
suite.Run(t, testingSuite)
|
||||
}
|
||||
|
||||
type softwareTestSuite struct {
|
||||
WithServer
|
||||
}
|
||||
|
||||
func (s *softwareTestSuite) SetupSuite() {
|
||||
s.WithServer.SetupSuite(s.T(), "androidSoftwareTestSuite")
|
||||
s.Token = "testtoken"
|
||||
}
|
||||
|
||||
func (s *softwareTestSuite) TestAndroidSoftwareIngestion() {
|
||||
ctx := context.Background()
|
||||
t := s.T()
|
||||
|
||||
// Create enterprise
|
||||
var signupResp android.EnterpriseSignupResponse
|
||||
s.DoJSON("GET", "/api/v1/fleet/android_enterprise/signup_url", nil, http.StatusOK, &signupResp)
|
||||
assert.Equal(s.T(), EnterpriseSignupURL, signupResp.Url)
|
||||
s.T().Logf("callbackURL: %s", s.ProxyCallbackURL)
|
||||
|
||||
s.FleetSvc.On("NewActivity", mock.Anything, mock.Anything, mock.AnythingOfType("fleet.ActivityTypeEnabledAndroidMDM")).Return(nil)
|
||||
const enterpriseToken = "enterpriseToken"
|
||||
s.Do("GET", s.ProxyCallbackURL, nil, http.StatusOK, "enterpriseToken", enterpriseToken)
|
||||
|
||||
// Update the LIST mock to return the enterprise after "creation"
|
||||
s.AndroidAPIClient.EnterprisesListFunc = func(_ context.Context, _ string) ([]*androidmanagement.Enterprise, error) {
|
||||
return []*androidmanagement.Enterprise{
|
||||
{Name: "enterprises/" + EnterpriseID},
|
||||
}, nil
|
||||
}
|
||||
|
||||
resp := android.GetEnterpriseResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/android_enterprise", nil, http.StatusOK, &resp)
|
||||
assert.Equal(t, EnterpriseID, resp.EnterpriseID)
|
||||
|
||||
// Need to set this because app config is mocked in this setup
|
||||
s.AppConfig.MDM.AndroidEnabledAndConfigured = true
|
||||
|
||||
err := s.DS.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: "enrollsecret"}})
|
||||
require.NoError(t, err)
|
||||
|
||||
secrets, err := s.DS.Datastore.GetEnrollSecrets(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, secrets, 1)
|
||||
|
||||
assets, err := s.DS.Datastore.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAndroidPubSubToken}, nil)
|
||||
require.NoError(t, err)
|
||||
pubsubToken := assets[fleet.MDMAssetAndroidPubSubToken]
|
||||
require.NotEmpty(t, pubsubToken.Value)
|
||||
|
||||
deviceID1 := createAndroidDeviceID("test-android")
|
||||
deviceID2 := createAndroidDeviceID("test-android-2")
|
||||
|
||||
enterpriseSpecificID1 := strings.ToUpper(uuid.New().String())
|
||||
enterpriseSpecificID2 := strings.ToUpper(uuid.New().String())
|
||||
var req service.PubSubPushRequest
|
||||
for _, d := range []struct {
|
||||
id string
|
||||
esi string
|
||||
}{{deviceID1, enterpriseSpecificID1}, {deviceID2, enterpriseSpecificID2}} {
|
||||
enrollmentMessage := enrollmentMessageWithEnterpriseSpecificID(
|
||||
t,
|
||||
androidmanagement.Device{
|
||||
Name: d.id,
|
||||
EnrollmentTokenData: fmt.Sprintf(`{"EnrollSecret": "%s"}`, secrets[0].Secret),
|
||||
},
|
||||
d.esi,
|
||||
)
|
||||
|
||||
req = service.PubSubPushRequest{
|
||||
PubSubMessage: *enrollmentMessage,
|
||||
}
|
||||
|
||||
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
|
||||
}
|
||||
|
||||
// Send device data including software for device 1
|
||||
software1 := []*androidmanagement.ApplicationReport{{
|
||||
DisplayName: "Google Chrome",
|
||||
PackageName: "com.google.chrome",
|
||||
VersionName: "1.0.0",
|
||||
State: "INSTALLED",
|
||||
}}
|
||||
deviceData1 := createAndroidDeviceWithSoftware(enterpriseSpecificID1, "test-android", createAndroidDeviceID("test-policy"), ptr.Int(1), nil, software1)
|
||||
req = service.PubSubPushRequest{
|
||||
PubSubMessage: createStatusReportMessageFromDevice(t, deviceData1),
|
||||
}
|
||||
|
||||
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
|
||||
|
||||
// Send device data including software for device 2
|
||||
software2 := []*androidmanagement.ApplicationReport{{
|
||||
DisplayName: "Google Chrome",
|
||||
PackageName: "com.google.chrome",
|
||||
VersionName: "2.0.0",
|
||||
State: "INSTALLED",
|
||||
}}
|
||||
deviceData2 := createAndroidDeviceWithSoftware(enterpriseSpecificID2, "test-android-2", createAndroidDeviceID("test-policy"), ptr.Int(1), nil, software2)
|
||||
req = service.PubSubPushRequest{
|
||||
PubSubMessage: createStatusReportMessageFromDevice(t, deviceData2),
|
||||
}
|
||||
|
||||
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
|
||||
|
||||
mysql.ExecAdhocSQL(t, s.DS.Datastore, func(q sqlx.ExtContext) error {
|
||||
// Check software table for correct values, we should have as many rows as there were ApplicationReports sent
|
||||
var software []*fleet.Software
|
||||
err := sqlx.SelectContext(ctx, q, &software, "SELECT id, name, application_id, source, title_id FROM software")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, software, len(deviceData1.ApplicationReports)+len(deviceData2.ApplicationReports))
|
||||
|
||||
// Check software_titles, we should have fewer rows here, because some ApplicationRows map to the same title.
|
||||
var titles []fleet.SoftwareTitle
|
||||
err = sqlx.SelectContext(ctx, q, &titles, "SELECT id, name, application_id, source FROM software_titles")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, titles, 1)
|
||||
|
||||
for _, got := range software {
|
||||
// Validate that both softwares map to the same title
|
||||
assert.Equal(t, *got.TitleID, titles[0].ID)
|
||||
|
||||
// Check other fields are as expected
|
||||
assert.Equal(t, "com.google.chrome", *got.ApplicationID)
|
||||
assert.Equal(t, "Google Chrome", got.Name)
|
||||
assert.Equal(t, "android_apps", got.Source)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Add some new software for the first device
|
||||
software1 = []*androidmanagement.ApplicationReport{
|
||||
{
|
||||
DisplayName: "Google Chrome",
|
||||
PackageName: "com.google.chrome",
|
||||
VersionName: "1.0.0",
|
||||
State: "INSTALLED",
|
||||
},
|
||||
{
|
||||
DisplayName: "YouTube",
|
||||
PackageName: "com.google.youtube",
|
||||
VersionName: "1.0.0",
|
||||
State: "INSTALLED",
|
||||
},
|
||||
{
|
||||
DisplayName: "Google Drive",
|
||||
PackageName: "com.google.drive",
|
||||
VersionName: "1.0.0",
|
||||
State: "INSTALLED",
|
||||
},
|
||||
}
|
||||
deviceData1 = createAndroidDeviceWithSoftware(enterpriseSpecificID1, "test-android", createAndroidDeviceID("test-policy"), ptr.Int(1), nil, software1)
|
||||
req = service.PubSubPushRequest{
|
||||
PubSubMessage: createStatusReportMessageFromDevice(t, deviceData1),
|
||||
}
|
||||
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
|
||||
|
||||
var hostID1 uint
|
||||
mysql.ExecAdhocSQL(t, s.DS.Datastore, func(q sqlx.ExtContext) error {
|
||||
// Get host id
|
||||
err := sqlx.GetContext(ctx, q, &hostID1, "SELECT id FROM hosts WHERE uuid = ?", enterpriseSpecificID1)
|
||||
require.NoError(t, err)
|
||||
|
||||
var software []*fleet.Software
|
||||
err = sqlx.SelectContext(ctx, q, &software, "SELECT id, name, application_id, source, title_id FROM software JOIN host_software ON host_software.software_id = software.id WHERE host_software.host_id = ?", hostID1)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, software, len(deviceData1.ApplicationReports)) // chrome, youtube, google drive
|
||||
|
||||
expectedSW := map[string]fleet.Software{
|
||||
"com.google.chrome": {ApplicationID: ptr.String("com.google.chrome"), Name: "Google Chrome", Source: "android_apps"},
|
||||
"com.google.youtube": {ApplicationID: ptr.String("com.google.youtube"), Name: "YouTube", Source: "android_apps"},
|
||||
"com.google.drive": {ApplicationID: ptr.String("com.google.drive"), Name: "Google Drive", Source: "android_apps"},
|
||||
}
|
||||
|
||||
for _, got := range software {
|
||||
// Check other fields are as expected
|
||||
require.NotNil(t, got.ApplicationID)
|
||||
expected, ok := expectedSW[*got.ApplicationID]
|
||||
require.Truef(t, ok, "got unexpected software with application id %s", got.ApplicationID)
|
||||
|
||||
assert.Equal(t, *expected.ApplicationID, *got.ApplicationID)
|
||||
assert.Equal(t, expected.Name, got.Name)
|
||||
assert.Equal(t, expected.Source, got.Source)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Remove some software from the first device
|
||||
software1 = []*androidmanagement.ApplicationReport{
|
||||
{
|
||||
DisplayName: "YouTube",
|
||||
PackageName: "com.google.youtube",
|
||||
VersionName: "1.0.0",
|
||||
State: "INSTALLED",
|
||||
},
|
||||
}
|
||||
deviceData1 = createAndroidDeviceWithSoftware(enterpriseSpecificID1, "test-android", createAndroidDeviceID("test-policy"), ptr.Int(1), nil, software1)
|
||||
req = service.PubSubPushRequest{
|
||||
PubSubMessage: createStatusReportMessageFromDevice(t, deviceData1),
|
||||
}
|
||||
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
|
||||
|
||||
mysql.ExecAdhocSQL(t, s.DS.Datastore, func(q sqlx.ExtContext) error {
|
||||
var software []*fleet.Software
|
||||
err = sqlx.SelectContext(ctx, q, &software, "SELECT id, name, application_id, source, title_id FROM software JOIN host_software ON host_software.software_id = software.id WHERE host_software.host_id = ?", hostID1)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, software, len(deviceData1.ApplicationReports)) // just youtube now
|
||||
|
||||
expectedSW := map[string]fleet.Software{
|
||||
"com.google.youtube": {ApplicationID: ptr.String("com.google.youtube"), Name: "YouTube", Source: "android_apps"},
|
||||
}
|
||||
|
||||
for _, got := range software {
|
||||
// Check other fields are as expected
|
||||
require.NotNil(t, got.ApplicationID)
|
||||
expected, ok := expectedSW[*got.ApplicationID]
|
||||
require.Truef(t, ok, "got unexpected software with application id %s", got.ApplicationID)
|
||||
|
||||
assert.Equal(t, *expected.ApplicationID, *got.ApplicationID)
|
||||
assert.Equal(t, expected.Name, got.Name)
|
||||
assert.Equal(t, expected.Source, got.Source)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Send the same software again, nothing changes
|
||||
deviceData1 = createAndroidDeviceWithSoftware(enterpriseSpecificID1, "test-android", createAndroidDeviceID("test-policy"), ptr.Int(1), nil, software1)
|
||||
req = service.PubSubPushRequest{
|
||||
PubSubMessage: createStatusReportMessageFromDevice(t, deviceData1),
|
||||
}
|
||||
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
|
||||
|
||||
mysql.ExecAdhocSQL(t, s.DS.Datastore, func(q sqlx.ExtContext) error {
|
||||
var software []*fleet.Software
|
||||
err = sqlx.SelectContext(ctx, q, &software, "SELECT id, name, application_id, source, title_id FROM software JOIN host_software ON host_software.software_id = software.id WHERE host_software.host_id = ?", hostID1)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, software, len(deviceData1.ApplicationReports)) // just youtube now
|
||||
|
||||
expectedSW := map[string]fleet.Software{
|
||||
"com.google.youtube": {ApplicationID: ptr.String("com.google.youtube"), Name: "YouTube", Source: "android_apps"},
|
||||
}
|
||||
|
||||
for _, got := range software {
|
||||
// Check other fields are as expected
|
||||
require.NotNil(t, got.ApplicationID)
|
||||
expected, ok := expectedSW[*got.ApplicationID]
|
||||
require.Truef(t, ok, "got unexpected software with application id %s", got.ApplicationID)
|
||||
|
||||
assert.Equal(t, expected.ApplicationID, got.ApplicationID)
|
||||
assert.Equal(t, expected.Name, got.Name)
|
||||
assert.Equal(t, expected.Source, got.Source)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func enrollmentMessageWithEnterpriseSpecificID(t *testing.T, deviceInfo androidmanagement.Device, enterpriseSpecificID string) *android.PubSubMessage {
|
||||
deviceInfo.HardwareInfo = &androidmanagement.HardwareInfo{
|
||||
EnterpriseSpecificId: enterpriseSpecificID,
|
||||
Brand: "TestBrand",
|
||||
Model: "TestModel",
|
||||
SerialNumber: "test-serial",
|
||||
Hardware: "test-hardware",
|
||||
}
|
||||
deviceInfo.SoftwareInfo = &androidmanagement.SoftwareInfo{
|
||||
AndroidBuildNumber: "test-build",
|
||||
AndroidVersion: "1",
|
||||
}
|
||||
deviceInfo.MemoryInfo = &androidmanagement.MemoryInfo{
|
||||
TotalRam: int64(8 * 1024 * 1024 * 1024), // 8GB RAM in bytes
|
||||
TotalInternalStorage: int64(64 * 1024 * 1024 * 1024), // 64GB system partition
|
||||
}
|
||||
|
||||
deviceInfo.MemoryEvents = []*androidmanagement.MemoryEvent{
|
||||
{
|
||||
EventType: "EXTERNAL_STORAGE_DETECTED",
|
||||
ByteCount: int64(64 * 1024 * 1024 * 1024), // 64GB external/built-in storage total capacity
|
||||
CreateTime: "2024-01-15T09:00:00Z",
|
||||
},
|
||||
{
|
||||
EventType: "INTERNAL_STORAGE_MEASURED",
|
||||
ByteCount: int64(10 * 1024 * 1024 * 1024), // 10GB free in system partition
|
||||
CreateTime: "2024-01-15T10:00:00Z",
|
||||
},
|
||||
{
|
||||
EventType: "EXTERNAL_STORAGE_MEASURED",
|
||||
ByteCount: int64(25 * 1024 * 1024 * 1024), // 25GB free in external/built-in storage
|
||||
CreateTime: "2024-01-15T10:00:00Z",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(deviceInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
encodedData := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
return &android.PubSubMessage{
|
||||
Attributes: map[string]string{
|
||||
"notificationType": string(android.PubSubEnrollment),
|
||||
},
|
||||
Data: encodedData,
|
||||
}
|
||||
}
|
||||
|
||||
func createAndroidDeviceID(name string) string {
|
||||
return "enterprises/mock-enterprise-id/devices/" + name
|
||||
}
|
||||
|
||||
func createAndroidDeviceWithSoftware(deviceId, name, policyName string, policyVersion *int, nonComplianceDetails []*androidmanagement.NonComplianceDetail, software []*androidmanagement.ApplicationReport) androidmanagement.Device {
|
||||
return androidmanagement.Device{
|
||||
Name: createAndroidDeviceID(name),
|
||||
NonComplianceDetails: nonComplianceDetails,
|
||||
HardwareInfo: &androidmanagement.HardwareInfo{
|
||||
EnterpriseSpecificId: deviceId,
|
||||
Brand: "TestBrand",
|
||||
Model: "TestModel",
|
||||
SerialNumber: "test-serial",
|
||||
Hardware: "test-hardware",
|
||||
},
|
||||
SoftwareInfo: &androidmanagement.SoftwareInfo{
|
||||
AndroidBuildNumber: "test-build",
|
||||
AndroidVersion: "1",
|
||||
},
|
||||
MemoryInfo: &androidmanagement.MemoryInfo{
|
||||
TotalRam: int64(8 * 1024 * 1024 * 1024), // 8GB RAM in bytes
|
||||
TotalInternalStorage: int64(64 * 1024 * 1024 * 1024), // 64GB system partition
|
||||
},
|
||||
AppliedPolicyName: policyName,
|
||||
AppliedPolicyVersion: int64(*policyVersion),
|
||||
LastPolicySyncTime: "2001-01-01T00:00:00Z",
|
||||
ApplicationReports: software,
|
||||
}
|
||||
}
|
||||
|
||||
func createStatusReportMessageFromDevice(t *testing.T, device androidmanagement.Device) android.PubSubMessage {
|
||||
data, err := json.Marshal(device)
|
||||
require.NoError(t, err)
|
||||
|
||||
encodedData := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
return android.PubSubMessage{
|
||||
Attributes: map[string]string{
|
||||
"notificationType": string(android.PubSubStatusReport),
|
||||
},
|
||||
Data: encodedData,
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ func (ts *WithServer) SetupSuite(t *testing.T, dbName string) {
|
|||
ts.createCommonProxyMocks(t)
|
||||
|
||||
logger := kitlog.NewLogfmtLogger(os.Stdout)
|
||||
svc, err := service.NewServiceWithClient(logger, &ts.DS, &ts.AndroidAPIClient, &ts.FleetSvc, "test-private-key")
|
||||
svc, err := service.NewServiceWithClient(logger, &ts.DS, &ts.AndroidAPIClient, &ts.FleetSvc, "test-private-key", ts.DS.Datastore)
|
||||
require.NoError(t, err)
|
||||
ts.Svc = svc
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
|
|||
&proxy,
|
||||
fleetSvc,
|
||||
"test-private-key",
|
||||
ds,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
androidSvc.(*android_service.Service).AllowLocalhostServerURL = true
|
||||
|
|
|
|||
Loading…
Reference in a new issue