feat(cli): convert JSON reports into a different format (#4452)

Co-authored-by: Aurelien LAJOIE <aurelien.lajoie@kili-technology.com>
This commit is contained in:
Teppei Fukuda
2023-05-24 11:45:26 +03:00
committed by GitHub
parent 09db1d4389
commit 50fe43f14c
15 changed files with 316 additions and 64 deletions

View File

@@ -1,5 +1,6 @@
# Reporting
## Supported Formats
Trivy supports the following formats:
- Table
@@ -8,7 +9,7 @@ Trivy supports the following formats:
- Template
- SBOM
## Table (Default)
### Table (Default)
| Scanner | Supported |
|:----------------:|:---------:|
@@ -21,7 +22,7 @@ Trivy supports the following formats:
$ trivy image -f table golang:1.12-alpine
```
### Show origins of vulnerable dependencies
#### Show origins of vulnerable dependencies
| Scanner | Supported |
|:----------------:|:---------:|
@@ -105,7 +106,7 @@ Also, **glob-parent@3.1.0** with some vulnerabilities is included through chain
Then, you can try to update **axios@0.21.4** and **cra-append-sw@2.7.0** to resolve vulnerabilities in **follow-redirects@1.14.6** and **glob-parent@3.1.0**.
## JSON
### JSON
| Scanner | Supported |
|:----------------:|:---------:|
@@ -239,7 +240,7 @@ $ trivy image -f json -o results.json golang:1.12-alpine
`VulnerabilityID`, `PkgName`, `InstalledVersion`, and `Severity` in `Vulnerabilities` are always filled with values, but other fields might be empty.
## SARIF
### SARIF
| Scanner | Supported |
|:----------------:|:---------:|
| Vulnerability | ✓ |
@@ -255,7 +256,7 @@ $ trivy image --format sarif -o report.sarif golang:1.12-alpine
This SARIF file can be uploaded to GitHub code scanning results, and there is a [Trivy GitHub Action][action] for automating this process.
## Template
### Template
| Scanner | Supported |
|:----------------:|:---------:|
@@ -264,7 +265,7 @@ This SARIF file can be uploaded to GitHub code scanning results, and there is a
| Secret | ✓ |
| License | ✓ |
### Custom Template
#### Custom Template
{% raw %}
```
@@ -301,18 +302,18 @@ Critical: 0, High: 2
For other features of sprig, see the official [sprig][sprig] documentation.
### Load templates from a file
#### Load templates from a file
You can load templates from a file prefixing the template path with an @.
```
$ trivy image --format template --template "@/path/to/template" golang:1.12-alpine
```
### Default Templates
#### Default Templates
If Trivy is installed using rpm then default templates can be found at `/usr/local/share/trivy/templates`.
#### JUnit
##### JUnit
| Scanner | Supported |
|:----------------:|:---------:|
| Vulnerability | ✓ |
@@ -325,7 +326,7 @@ In the following example using the template `junit.tpl` XML can be generated.
$ trivy image --format template --template "@contrib/junit.tpl" -o junit-report.xml golang:1.12-alpine
```
#### ASFF
##### ASFF
| Scanner | Supported |
|:----------------:|:---------:|
| Vulnerability | ✓ |
@@ -335,7 +336,7 @@ $ trivy image --format template --template "@contrib/junit.tpl" -o junit-report.
Trivy also supports an [ASFF template for reporting findings to AWS Security Hub][asff]
#### HTML
##### HTML
| Scanner | Supported |
|:----------------:|:---------:|
| Vulnerability | ✓ |
@@ -353,9 +354,34 @@ The following example shows use of default HTML template when Trivy is installed
$ trivy image --format template --template "@/usr/local/share/trivy/templates/html.tpl" -o report.html golang:1.12-alpine
```
## SBOM
### SBOM
See [here](../supply-chain/sbom.md) for details.
## Converting
To generate multiple reports, you can generate the JSON report first and convert it to other formats with the `convert` subcommand.
```shell
$ trivy image --format json -o result.json --list-all-pkgs debian:11
$ trivy convert --format cyclonedx --output result.cdx result.json
```
!!! note
Please note that if you want to convert to a format that requires a list of packages,
such as SBOM, you need to add the `--list-all-pkgs` flag when outputting in JSON.
[Filtering options](./filtering.md) such as `--severity` are also available with `convert`.
```shell
# Output all severities in JSON
$ trivy image --format json -o result.json --list-all-pkgs debian:11
# Output only critical issues in table format
$ trivy convert --format table --severity CRITICAL result.json
```
!!! note
JSON reports from "trivy aws" and "trivy k8s" are not yet supported.
[cargo-auditable]: https://github.com/rust-secure-code/cargo-auditable/
[action]: https://github.com/aquasecurity/trivy-action
[asff]: ../../tutorials/integrations/aws-security-hub.md

View File

@@ -45,6 +45,7 @@ trivy [global flags] command [flags] target
* [trivy aws](trivy_aws.md) - [EXPERIMENTAL] Scan AWS account
* [trivy config](trivy_config.md) - Scan config files for misconfigurations
* [trivy convert](trivy_convert.md) - Convert Trivy JSON report into a different format
* [trivy filesystem](trivy_filesystem.md) - Scan local filesystem
* [trivy image](trivy_image.md) - Scan a container image
* [trivy kubernetes](trivy_kubernetes.md) - [EXPERIMENTAL] Scan kubernetes cluster

View File

@@ -0,0 +1,52 @@
## trivy convert
Convert Trivy JSON report into a different format
```
trivy convert [flags] RESULT_JSON
```
### Examples
```
# report conversion
$ trivy image --format json --output result.json --list-all-pkgs debian:11
$ trivy convert --format cyclonedx --output result.cdx result.json
```
### Options
```
--compliance string compliance report to generate
--dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages
--exit-code int specify exit code when any security issues are found
--exit-on-eol int exit with the specified code when the OS reaches end of service/life
-f, --format string format (table, json, template, sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln) (default "table")
-h, --help help for convert
--ignore-policy string specify the Rego file path to evaluate each vulnerability
--ignorefile string specify .trivyignore file (default ".trivyignore")
--list-all-pkgs enabling the option will output all packages regardless of vulnerability
-o, --output string output file name
--report string specify a report format for the output. (all,summary) (default "all")
-s, --severity string severities of security issues to be displayed (comma separated) (default "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL")
-t, --template string output template
```
### Options inherited from parent commands
```
--cache-dir string cache directory (default "/path/to/cache")
-c, --config string config path (default "trivy.yaml")
-d, --debug debug mode
--generate-default-config write the default config to trivy-default.yaml
--insecure allow insecure server connections
-q, --quiet suppress progress bar and log output
--timeout duration timeout (default 5m0s)
-v, --version show version
```
### SEE ALSO
* [trivy](trivy.md) - Unified security scanner

View File

@@ -3,3 +3,9 @@
### How to pronounce the name "Trivy"?
`tri` is pronounced like **tri**gger, `vy` is pronounced like en**vy**.
### How to generate multiple reports?
See [here](../docs/configuration/reporting.md#converting).
### How to run Trivy under air-gapped environment?
See [here](../docs/advanced/air-gap.md).

View File

@@ -15,7 +15,7 @@ import (
"github.com/aquasecurity/trivy/pkg/cloud"
"github.com/aquasecurity/trivy/pkg/cloud/aws/scanner"
"github.com/aquasecurity/trivy/pkg/cloud/report"
cmd "github.com/aquasecurity/trivy/pkg/commands/artifact"
"github.com/aquasecurity/trivy/pkg/commands/operation"
cr "github.com/aquasecurity/trivy/pkg/compliance/report"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/log"
@@ -147,6 +147,6 @@ func Run(ctx context.Context, opt flag.Options) error {
return fmt.Errorf("unable to write results: %w", err)
}
cmd.Exit(opt, r.Failed())
operation.Exit(opt, r.Failed())
return nil
}

View File

@@ -19,7 +19,7 @@ const (
tableFormat = "table"
)
// Report represents a kubernetes scan report
// Report represents an AWS scan report
type Report struct {
Provider string
AccountID string

View File

@@ -19,6 +19,7 @@ import (
javadb "github.com/aquasecurity/trivy-java-db/pkg/db"
awscommands "github.com/aquasecurity/trivy/pkg/cloud/aws/commands"
"github.com/aquasecurity/trivy/pkg/commands/artifact"
"github.com/aquasecurity/trivy/pkg/commands/convert"
"github.com/aquasecurity/trivy/pkg/commands/server"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/flag"
@@ -106,6 +107,7 @@ func NewApp(version string) *cobra.Command {
NewClientCommand(globalFlags),
NewServerCommand(globalFlags),
NewConfigCommand(globalFlags),
NewConvertCommand(globalFlags),
NewPluginCommand(),
NewModuleCommand(globalFlags),
NewKubernetesCommand(globalFlags),
@@ -494,6 +496,47 @@ func NewRepositoryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
return cmd
}
func NewConvertCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
convertFlags := &flag.Flags{
ScanFlagGroup: &flag.ScanFlagGroup{},
ReportFlagGroup: flag.NewReportFlagGroup(),
}
cmd := &cobra.Command{
Use: "convert [flags] RESULT_JSON",
Aliases: []string{"conv"},
GroupID: groupUtility,
Short: "Convert Trivy JSON report into a different format",
Example: ` # report conversion
$ trivy image --format json --output result.json --list-all-pkgs debian:11
$ trivy convert --format cyclonedx --output result.cdx result.json
`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := convertFlags.Bind(cmd); err != nil {
return xerrors.Errorf("flag bind error: %w", err)
}
return validateArgs(cmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := convertFlags.Bind(cmd); err != nil {
return xerrors.Errorf("flag bind error: %w", err)
}
opts, err := convertFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter)
if err != nil {
return xerrors.Errorf("flag error: %w", err)
}
return convert.Run(cmd.Context(), opts)
},
SilenceErrors: true,
SilenceUsage: true,
}
cmd.SetFlagErrorFunc(flagErrorFunc)
convertFlags.AddFlags(cmd)
cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, convertFlags.Usages(cmd)))
return cmd
}
// NewClientCommand returns the 'client' subcommand that is deprecated
func NewClientCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
remoteFlags := flag.NewClientFlags()
@@ -799,7 +842,7 @@ func NewModuleCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
if err != nil {
return xerrors.Errorf("flag error: %w", err)
}
return module.Install(cmd.Context(), opts.ModuleDir, repo, opts.Quiet, opts.Registry())
return module.Install(cmd.Context(), opts.ModuleDir, repo, opts.Quiet, opts.RegistryOpts())
},
},
&cobra.Command{

View File

@@ -4,9 +4,6 @@ import (
"context"
"errors"
"fmt"
"os"
"github.com/aquasecurity/trivy/pkg/policy"
"github.com/hashicorp/go-multierror"
"github.com/spf13/viper"
@@ -26,6 +23,7 @@ import (
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/misconf"
"github.com/aquasecurity/trivy/pkg/module"
"github.com/aquasecurity/trivy/pkg/policy"
pkgReport "github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc/client"
@@ -273,15 +271,7 @@ func (r *runner) scanArtifact(ctx context.Context, opts flag.Options, initialize
func (r *runner) Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error) {
// Filter results
err := result.Filter(ctx, report, result.FilterOption{
Severities: opts.Severities,
IgnoreUnfixed: opts.IgnoreUnfixed,
IncludeNonFailures: opts.IncludeNonFailures,
IgnoreFile: opts.IgnoreFile,
PolicyFile: opts.IgnorePolicy,
IgnoreLicenses: opts.IgnoredLicenses,
VEXPath: opts.VEXPath,
})
err := result.Filter(ctx, report, opts.FilterOpts())
if err != nil {
return types.Report{}, xerrors.Errorf("filtering error: %w", err)
}
@@ -290,18 +280,7 @@ func (r *runner) Filter(ctx context.Context, opts flag.Options, report types.Rep
}
func (r *runner) Report(opts flag.Options, report types.Report) error {
if err := pkgReport.Write(report, pkgReport.Option{
AppVersion: opts.AppVersion,
Format: opts.Format,
Output: opts.Output,
Tree: opts.DependencyTree,
Severities: opts.Severities,
OutputTemplate: opts.Template,
IncludeNonFailures: opts.IncludeNonFailures,
Trace: opts.Trace,
Report: opts.ReportFormat,
Compliance: opts.Compliance,
}); err != nil {
if err := pkgReport.Write(report, opts.ReportOpts()); err != nil {
return xerrors.Errorf("unable to write results: %w", err)
}
@@ -320,7 +299,7 @@ func (r *runner) initDB(ctx context.Context, opts flag.Options) error {
// download the database file
noProgress := opts.Quiet || opts.NoProgress
if err := operation.DownloadDB(ctx, opts.AppVersion, opts.CacheDir, opts.DBRepository, noProgress, opts.SkipDBUpdate, opts.Registry()); err != nil {
if err := operation.DownloadDB(ctx, opts.AppVersion, opts.CacheDir, opts.DBRepository, noProgress, opts.SkipDBUpdate, opts.RegistryOpts()); err != nil {
return err
}
@@ -475,8 +454,8 @@ func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err err
return xerrors.Errorf("report error: %w", err)
}
exitOnEOL(opts, report.Metadata)
Exit(opts, report.Results.Failed())
operation.ExitOnEOL(opts, report.Metadata)
operation.Exit(opts, report.Results.Failed())
return nil
}
@@ -661,7 +640,7 @@ func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfi
// For image scanning
ImageOption: ftypes.ImageOptions{
RegistryOptions: opts.Registry(),
RegistryOptions: opts.RegistryOpts(),
DockerOptions: ftypes.DockerOptions{
Host: opts.DockerHost,
},
@@ -704,19 +683,6 @@ func scan(ctx context.Context, opts flag.Options, initializeScanner InitializeSc
return report, nil
}
func Exit(opts flag.Options, failedResults bool) {
if opts.ExitCode != 0 && failedResults {
os.Exit(opts.ExitCode)
}
}
func exitOnEOL(opts flag.Options, m types.Metadata) {
if opts.ExitOnEOL != 0 && m.OS != nil && m.OS.Eosl {
log.Logger.Errorf("Detected EOL OS: %s %s", m.OS.Family, m.OS.Name)
os.Exit(opts.ExitOnEOL)
}
}
func canonicalVersion(ver string) string {
if ver == devVersion {
return ver

View File

@@ -0,0 +1,48 @@
package convert
import (
"context"
"encoding/json"
"os"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/types"
)
func Run(ctx context.Context, opts flag.Options) (err error) {
f, err := os.Open(opts.Target)
if err != nil {
return xerrors.Errorf("file open error: %w", err)
}
defer f.Close()
var r types.Report
if err = json.NewDecoder(f).Decode(&r); err != nil {
return xerrors.Errorf("json decode error: %w", err)
}
// "convert" supports JSON results produced by Trivy scanning other than AWS and Kubernetes
if r.ArtifactName == "" && r.ArtifactType == "" {
return xerrors.New("AWS and Kubernetes scanning reports are not yet supported")
}
if err = result.Filter(ctx, r, opts.FilterOpts()); err != nil {
return xerrors.Errorf("unable to filter results: %w", err)
}
log.Logger.Debug("Writing report to output...")
if err = report.Write(r, opts.ReportOpts()); err != nil {
return xerrors.Errorf("unable to write results: %w", err)
}
operation.ExitOnEOL(opts, r.Metadata)
operation.Exit(opts, r.Results.Failed())
return nil
}

View File

@@ -16,10 +16,11 @@ import (
"github.com/aquasecurity/trivy-db/pkg/metadata"
"github.com/aquasecurity/trivy/pkg/db"
"github.com/aquasecurity/trivy/pkg/fanal/cache"
"github.com/aquasecurity/trivy/pkg/fanal/types"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/policy"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)
@@ -109,7 +110,7 @@ func (c Cache) ClearArtifacts() error {
}
// DownloadDB downloads the DB
func DownloadDB(ctx context.Context, appVersion, cacheDir, dbRepository string, quiet, skipUpdate bool, opt types.RegistryOptions) error {
func DownloadDB(ctx context.Context, appVersion, cacheDir, dbRepository string, quiet, skipUpdate bool, opt ftypes.RegistryOptions) error {
mu.Lock()
defer mu.Unlock()
@@ -201,3 +202,16 @@ func GetTLSConfig(caCertPath, certPath, keyPath string) (*x509.CertPool, tls.Cer
return caCertPool, cert, nil
}
func Exit(opts flag.Options, failedResults bool) {
if opts.ExitCode != 0 && failedResults {
os.Exit(opts.ExitCode)
}
}
func ExitOnEOL(opts flag.Options, m types.Metadata) {
if opts.ExitOnEOL != 0 && m.OS != nil && m.OS.Eosl {
log.Logger.Errorf("Detected EOL OS: %s %s", m.OS.Family, m.OS.Name)
os.Exit(opts.ExitOnEOL)
}
}

View File

@@ -35,7 +35,7 @@ func Run(ctx context.Context, opts flag.Options) (err error) {
// download the database file
if err = operation.DownloadDB(ctx, opts.AppVersion, opts.CacheDir, opts.DBRepository,
true, opts.SkipDBUpdate, opts.Registry()); err != nil {
true, opts.SkipDBUpdate, opts.RegistryOpts()); err != nil {
return err
}
@@ -58,6 +58,6 @@ func Run(ctx context.Context, opts flag.Options) (err error) {
m.Register()
server := rpcServer.NewServer(opts.AppVersion, opts.Listen, opts.CacheDir, opts.Token, opts.TokenHeader,
opts.DBRepository, opts.Registry())
opts.DBRepository, opts.RegistryOpts())
return server.ListenAndServe(cache, opts.SkipDBUpdate)
}

View File

@@ -18,6 +18,7 @@ import (
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/result"
)
type Flag struct {
@@ -122,8 +123,8 @@ func (o *Options) Align() {
}
}
// Registry returns options for OCI registries
func (o *Options) Registry() ftypes.RegistryOptions {
// RegistryOpts returns options for OCI registries
func (o *Options) RegistryOpts() ftypes.RegistryOptions {
return ftypes.RegistryOptions{
Credentials: o.Credentials,
RegistryToken: o.RegistryToken,
@@ -133,6 +134,34 @@ func (o *Options) Registry() ftypes.RegistryOptions {
}
}
// FilterOpts returns options for filtering
func (o *Options) FilterOpts() result.FilterOption {
return result.FilterOption{
Severities: o.Severities,
IgnoreUnfixed: o.IgnoreUnfixed,
IncludeNonFailures: o.IncludeNonFailures,
IgnoreFile: o.IgnoreFile,
PolicyFile: o.IgnorePolicy,
IgnoreLicenses: o.IgnoredLicenses,
VEXPath: o.VEXPath,
}
}
func (o *Options) ReportOpts() report.Option {
return report.Option{
AppVersion: o.AppVersion,
Format: o.Format,
Output: o.Output,
Tree: o.DependencyTree,
Severities: o.Severities,
OutputTemplate: o.Template,
IncludeNonFailures: o.IncludeNonFailures,
Trace: o.Trace,
Report: o.ReportFormat,
Compliance: o.Compliance,
}
}
func addFlag(cmd *cobra.Command, flag *Flag) {
if flag == nil || flag.Name == "" {
return

View File

@@ -10,6 +10,7 @@ import (
"github.com/aquasecurity/trivy-kubernetes/pkg/artifacts"
"github.com/aquasecurity/trivy-kubernetes/pkg/k8s"
cmd "github.com/aquasecurity/trivy/pkg/commands/artifact"
"github.com/aquasecurity/trivy/pkg/commands/operation"
cr "github.com/aquasecurity/trivy/pkg/compliance/report"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/k8s/report"
@@ -120,7 +121,7 @@ func (r *runner) run(ctx context.Context, artifacts []*artifacts.Artifact) error
return xerrors.Errorf("unable to write results: %w", err)
}
cmd.Exit(r.flagOpts, rpt.Failed())
operation.Exit(r.flagOpts, rpt.Failed())
return nil
}

View File

@@ -139,6 +139,9 @@ func (m *Marshaler) Marshal(r types.Report) (*spdx.Document, error) {
)
for _, result := range r.Results {
if len(result.Packages) == 0 {
continue
}
parentPackage, err := m.resultToSpdxPackage(result, r.Metadata.OS, pkgDownloadLocation)
if err != nil {
return nil, xerrors.Errorf("failed to parse result: %w", err)

View File

@@ -761,6 +761,69 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
},
{
name: "happy path secret",
inputReport: types.Report{
SchemaVersion: report.SchemaVersion,
ArtifactName: "secret",
ArtifactType: ftypes.ArtifactFilesystem,
Results: types.Results{
{
Target: "key.pem",
Class: types.ClassSecret,
Secrets: []ftypes.SecretFinding{
{
RuleID: "private-key",
Category: "AsymmetricPrivateKey",
Severity: "HIGH",
Title: "Asymmetric Private Key",
StartLine: 1,
EndLine: 1,
},
},
},
},
},
wantSBOM: &spdx.Document{
SPDXVersion: spdx.Version,
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "secret",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/secret-3ff14136-e09f-4df9-80ea-000000000001",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
Creator: "aquasecurity",
CreatorType: "Organization",
},
{
Creator: fmt.Sprintf("trivy-0.38.1"),
CreatorType: "Tool",
},
},
Created: "2021-08-25T12:20:30Z",
},
Packages: []*spdx.Package{
{
PackageName: "secret",
PackageSPDXIdentifier: "Filesystem-5c08d34162a2c5d3",
PackageDownloadLocation: "NONE",
PackageAttributionTexts: []string{
"SchemaVersion: 2",
},
PrimaryPackagePurpose: tspdx.PackagePurposeSource,
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"},
RefB: spdx.DocElementID{ElementRefID: "Filesystem-5c08d34162a2c5d3"},
Relationship: "DESCRIBES",
},
},
},
},
}
clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))