mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-23 07:29:00 -08:00
fix(config): change selector type (fanal#189)
* fix(config): change selector type * test(policy): fix test data
This commit is contained in:
@@ -60,6 +60,11 @@ func run() (err error) {
|
|||||||
Aliases: []string{"fs"},
|
Aliases: []string{"fs"},
|
||||||
Usage: "inspect a local directory",
|
Usage: "inspect a local directory",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "namespace",
|
||||||
|
Usage: "namespaces",
|
||||||
|
Value: cli.NewStringSlice("appshield"),
|
||||||
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "policy",
|
Name: "policy",
|
||||||
Usage: "policy paths",
|
Usage: "policy paths",
|
||||||
@@ -141,6 +146,7 @@ func archiveAction(c *cli.Context, fsCache cache.Cache) error {
|
|||||||
|
|
||||||
func fsAction(c *cli.Context, fsCache cache.Cache) error {
|
func fsAction(c *cli.Context, fsCache cache.Cache) error {
|
||||||
art, err := local.NewArtifact(c.Args().First(), fsCache, nil, config.ScannerOption{
|
art, err := local.NewArtifact(c.Args().First(), fsCache, nil, config.ScannerOption{
|
||||||
|
Namespaces: []string{"appshield"},
|
||||||
PolicyPaths: c.StringSlice("policy"),
|
PolicyPaths: c.StringSlice("policy"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -189,6 +195,9 @@ func inspect(ctx context.Context, art artifact.Artifact, c cache.LocalArtifactCa
|
|||||||
}
|
}
|
||||||
for _, misconf := range mergedLayer.Misconfigurations {
|
for _, misconf := range mergedLayer.Misconfigurations {
|
||||||
fmt.Printf(" %s: failures %d, warnings %d\n", misconf.FilePath, len(misconf.Failures), len(misconf.Warnings))
|
fmt.Printf(" %s: failures %d, warnings %d\n", misconf.FilePath, len(misconf.Failures), len(misconf.Warnings))
|
||||||
|
for _, failure := range misconf.Failures {
|
||||||
|
fmt.Printf(" %s: %s\n", failure.ID, failure.Message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,13 +196,13 @@ func (e *Engine) Check(ctx context.Context, configs []types.Config, namespaces [
|
|||||||
}
|
}
|
||||||
|
|
||||||
var selectedConfigs []types.Config
|
var selectedConfigs []types.Config
|
||||||
if len(inputOption.Selector.Types) > 0 {
|
if len(inputOption.Selectors) > 0 {
|
||||||
// Pass only the config files that match the selector types
|
// Pass only the config files that match the selector types
|
||||||
for _, t := range inputOption.Selector.Types {
|
for _, t := range uniqueSelectorTypes(inputOption.Selectors) {
|
||||||
selectedConfigs = append(selectedConfigs, typedConfigs[t]...)
|
selectedConfigs = append(selectedConfigs, typedConfigs[t]...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// When the 'types' is not specified, it means '*'.
|
// When the 'selector' is not specified, it means '*'.
|
||||||
selectedConfigs = configs
|
selectedConfigs = configs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,6 +666,14 @@ func removeRulePrefix(rule string) string {
|
|||||||
return rule
|
return rule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func uniqueSelectorTypes(selectors []types.PolicyInputSelector) []string {
|
||||||
|
selectorTypes := map[string]struct{}{}
|
||||||
|
for _, s := range selectors {
|
||||||
|
selectorTypes[s.Type] = struct{}{}
|
||||||
|
}
|
||||||
|
return utils.Keys(selectorTypes)
|
||||||
|
}
|
||||||
|
|
||||||
func uniqueResults(results []types.MisconfResult) []types.MisconfResult {
|
func uniqueResults(results []types.MisconfResult) []types.MisconfResult {
|
||||||
uniq := map[string]types.MisconfResult{}
|
uniq := map[string]types.MisconfResult{}
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ __rego_metadata__ := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {"types": ["kubernetes"]},
|
|
||||||
"combine": true,
|
"combine": true,
|
||||||
|
"selector": [{"type": "kubernetes"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[res] {
|
deny[res] {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ __rego_metadata__ := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {"types": ["dockerfile"]},
|
|
||||||
"combine": true,
|
"combine": true,
|
||||||
|
"selector": [{"type": "dockerfile"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[res] {
|
deny[res] {
|
||||||
18
policy/testdata/combine/combined_pod.rego
vendored
18
policy/testdata/combine/combined_pod.rego
vendored
@@ -1,22 +1,20 @@
|
|||||||
package testdata.xyz_400
|
package testdata.xyz_400
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"id": "XYZ-400",
|
"id": "XYZ-400",
|
||||||
"title": "Bad Combined Pod",
|
"title": "Bad Combined Pod",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"severity": "LOW",
|
"severity": "LOW",
|
||||||
"type": "Kubernetes Security Check",
|
"type": "Kubernetes Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {
|
"combine": true,
|
||||||
"types": ["kubernetes"]
|
"selector": [{"type": "kubernetes"}],
|
||||||
},
|
|
||||||
"combine": true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[res] {
|
deny[res] {
|
||||||
input[i].contents.kind == "Pod"
|
input[i].contents.kind == "Pod"
|
||||||
res := {
|
res := {
|
||||||
"filepath": input[i].path,
|
"filepath": input[i].path,
|
||||||
"msg": sprintf("deny combined %s", [input[i].contents.metadata.name]),
|
"msg": sprintf("deny combined %s", [input[i].contents.metadata.name]),
|
||||||
|
|||||||
2
policy/testdata/combine/deployment.rego
vendored
2
policy/testdata/combine/deployment.rego
vendored
@@ -11,8 +11,8 @@ __rego_metadata__ := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {"types": ["kubernetes"]},
|
|
||||||
"combine": false,
|
"combine": false,
|
||||||
|
"selector": [{"type": "kubernetes"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
warn[msg] {
|
warn[msg] {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ __rego_metadata__ := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {"types": ["kubernetes"]},
|
|
||||||
"combine": true,
|
"combine": true,
|
||||||
|
"selector": [{"type": "kubernetes"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
warn[res] {
|
warn[res] {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ __rego_metadata__ := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {"types": ["kubernetes"]},
|
|
||||||
"combine": false,
|
"combine": false,
|
||||||
|
"selector": [{"type": "kubernetes"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
warn[msg] {
|
warn[msg] {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package namespace.exceptions
|
|||||||
import data.namespaces
|
import data.namespaces
|
||||||
|
|
||||||
exception[ns] {
|
exception[ns] {
|
||||||
ns := data.namespaces[_]
|
ns := data.namespaces[_]
|
||||||
ns == "testdata.xyz_300"
|
ns == "testdata.xyz_300"
|
||||||
}
|
}
|
||||||
|
|||||||
2
policy/testdata/combine_exception/fail.rego
vendored
2
policy/testdata/combine_exception/fail.rego
vendored
@@ -11,8 +11,8 @@ __rego_metadata__ := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {"types": ["kubernetes"]},
|
|
||||||
"combine": true,
|
"combine": true,
|
||||||
|
"selector": [{"type": "kubernetes"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[res] {
|
deny[res] {
|
||||||
|
|||||||
22
policy/testdata/happy/deployment.rego
vendored
22
policy/testdata/happy/deployment.rego
vendored
@@ -3,22 +3,20 @@ package testdata.xyz_100
|
|||||||
import data.services
|
import data.services
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"id": "XYZ-100",
|
"id": "XYZ-100",
|
||||||
"title": "Bad Deployment",
|
"title": "Bad Deployment",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"severity": "HIGH",
|
"severity": "HIGH",
|
||||||
"type": "Kubernetes Security Check",
|
"type": "Kubernetes Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {
|
"combine": false,
|
||||||
"types": ["kubernetes"]
|
"selector": [{"type": "kubernetes"}],
|
||||||
},
|
|
||||||
"combine": false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[msg] {
|
deny[msg] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
services.ports[_] == 22
|
services.ports[_] == 22
|
||||||
msg := sprintf("deny %s", [input.metadata.name])
|
msg := sprintf("deny %s", [input.metadata.name])
|
||||||
}
|
}
|
||||||
|
|||||||
18
policy/testdata/happy/docker.rego
vendored
18
policy/testdata/happy/docker.rego
vendored
@@ -1,20 +1,18 @@
|
|||||||
package testdata.xyz_200
|
package testdata.xyz_200
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"id": "XYZ-200",
|
"id": "XYZ-200",
|
||||||
"title": "Bad FROM",
|
"title": "Bad FROM",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"severity": "LOW",
|
"severity": "LOW",
|
||||||
"type": "Docker Security Check",
|
"type": "Docker Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {
|
"combine": false,
|
||||||
"types": ["dockerfile"]
|
"selector": [{"type": "dockerfile"}],
|
||||||
},
|
|
||||||
"combine": false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[msg] {
|
deny[msg] {
|
||||||
msg := "bad Dockerfile"
|
msg := "bad Dockerfile"
|
||||||
}
|
}
|
||||||
|
|||||||
20
policy/testdata/happy/pod.rego
vendored
20
policy/testdata/happy/pod.rego
vendored
@@ -1,21 +1,19 @@
|
|||||||
package testdata.xyz_300
|
package testdata.xyz_300
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"id": "XYZ-300",
|
"id": "XYZ-300",
|
||||||
"title": "Bad Pod",
|
"title": "Bad Pod",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"severity": "CRITICAL",
|
"severity": "CRITICAL",
|
||||||
"type": "Kubernetes Security Check",
|
"type": "Kubernetes Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {
|
"combine": false,
|
||||||
"types": ["kubernetes"]
|
"selector": [{"type": "kubernetes"}],
|
||||||
},
|
|
||||||
"combine": false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[msg] {
|
deny[msg] {
|
||||||
input.kind == "Pod"
|
input.kind == "Pod"
|
||||||
msg := sprintf("deny %s", [input.metadata.name])
|
msg := sprintf("deny %s", [input.metadata.name])
|
||||||
}
|
}
|
||||||
|
|||||||
14
policy/testdata/namespace_exception/100.rego
vendored
14
policy/testdata/namespace_exception/100.rego
vendored
@@ -1,14 +1,14 @@
|
|||||||
package testdata.kubernetes.xyz_100
|
package testdata.kubernetes.xyz_100
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"id": "XYZ-100",
|
"id": "XYZ-100",
|
||||||
"title": "Bad Deployment",
|
"title": "Bad Deployment",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"severity": "HIGH",
|
"severity": "HIGH",
|
||||||
"type": "Kubernetes Security Check",
|
"type": "Kubernetes Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[msg] {
|
deny[msg] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
msg := sprintf("deny 100 %s", [input.metadata.name])
|
msg := sprintf("deny 100 %s", [input.metadata.name])
|
||||||
}
|
}
|
||||||
14
policy/testdata/namespace_exception/200.rego
vendored
14
policy/testdata/namespace_exception/200.rego
vendored
@@ -1,14 +1,14 @@
|
|||||||
package testdata.kubernetes.xyz_200
|
package testdata.kubernetes.xyz_200
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"id": "XYZ-200",
|
"id": "XYZ-200",
|
||||||
"title": "Bad Deployment",
|
"title": "Bad Deployment",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"severity": "HIGH",
|
"severity": "HIGH",
|
||||||
"type": "Kubernetes Security Check",
|
"type": "Kubernetes Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[res] {
|
deny[res] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
res := {"msg": sprintf("deny 200 %s", [input.metadata.name])}
|
res := {"msg": sprintf("deny 200 %s", [input.metadata.name])}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,6 @@ package namespace.exceptions
|
|||||||
import data.namespaces
|
import data.namespaces
|
||||||
|
|
||||||
exception[ns] {
|
exception[ns] {
|
||||||
ns := data.namespaces[_]
|
ns := data.namespaces[_]
|
||||||
ns == "testdata.kubernetes.xyz_100"
|
ns == "testdata.kubernetes.xyz_100"
|
||||||
}
|
}
|
||||||
|
|||||||
20
policy/testdata/rule_exception/deployment.rego
vendored
20
policy/testdata/rule_exception/deployment.rego
vendored
@@ -1,23 +1,23 @@
|
|||||||
package testdata.kubernetes.xyz_100
|
package testdata.kubernetes.xyz_100
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"id": "XYZ-100",
|
"id": "XYZ-100",
|
||||||
"title": "Bad Deployment",
|
"title": "Bad Deployment",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"severity": "HIGH",
|
"severity": "HIGH",
|
||||||
"type": "Kubernetes Security Check",
|
"type": "Kubernetes Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
deny_foo[msg] {
|
deny_foo[msg] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
msg := sprintf("deny foo %s", [input.metadata.name])
|
msg := sprintf("deny foo %s", [input.metadata.name])
|
||||||
}
|
}
|
||||||
|
|
||||||
deny_bar[msg] {
|
deny_bar[msg] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
msg := sprintf("deny bar %s", [input.metadata.name])
|
msg := sprintf("deny bar %s", [input.metadata.name])
|
||||||
}
|
}
|
||||||
|
|
||||||
exception[rules] {
|
exception[rules] {
|
||||||
rules = ["foo"]
|
rules = ["foo"]
|
||||||
}
|
}
|
||||||
4
policy/testdata/sad/broken_metadata.rego
vendored
4
policy/testdata/sad/broken_metadata.rego
vendored
@@ -3,6 +3,6 @@ package testdata.kubernetes.xyz_100
|
|||||||
__rego_metadata__ := "broken"
|
__rego_metadata__ := "broken"
|
||||||
|
|
||||||
deny[msg] {
|
deny[msg] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
msg := sprintf("deny %s", [input.metadata.name])
|
msg := sprintf("deny %s", [input.metadata.name])
|
||||||
}
|
}
|
||||||
|
|||||||
14
policy/testdata/sad/broken_msg.rego
vendored
14
policy/testdata/sad/broken_msg.rego
vendored
@@ -1,14 +1,14 @@
|
|||||||
package testdata.kubernetes.xyz_200
|
package testdata.kubernetes.xyz_200
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"id": "XYZ-200",
|
"id": "XYZ-200",
|
||||||
"title": "Bad Deployment",
|
"title": "Bad Deployment",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"severity": "HIGH",
|
"severity": "HIGH",
|
||||||
"type": "Kubernetes Security Check",
|
"type": "Kubernetes Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[res] {
|
deny[res] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
res := {"hello": "world"}
|
res := {"hello": "world"}
|
||||||
}
|
}
|
||||||
2
policy/testdata/sad/broken_rule.rego
vendored
2
policy/testdata/sad/broken_rule.rego
vendored
@@ -1,5 +1,5 @@
|
|||||||
package testdata.kubernetes.xyz_100
|
package testdata.kubernetes.xyz_100
|
||||||
|
|
||||||
deny[msg] {
|
deny[msg] {
|
||||||
broken
|
broken
|
||||||
}
|
}
|
||||||
|
|||||||
6
policy/testdata/sad/missing_filepath.rego
vendored
6
policy/testdata/sad/missing_filepath.rego
vendored
@@ -11,14 +11,12 @@ __rego_metadata__ := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__rego_input__ := {
|
__rego_input__ := {
|
||||||
"selector": {"types": ["kubernetes"]},
|
|
||||||
"combine": true,
|
"combine": true,
|
||||||
|
"selector": [{"type": "kubernetes"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
warn[res] {
|
warn[res] {
|
||||||
input[i].contents.kind == "Deployment"
|
input[i].contents.kind == "Deployment"
|
||||||
services.ports[_] == 22
|
services.ports[_] == 22
|
||||||
res := {
|
res := {"msg": sprintf("deny combined %s", [input[i].contents.metadata.name])}
|
||||||
"msg": sprintf("deny combined %s", [input[i].contents.metadata.name]),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
4
policy/testdata/sad/missing_metadata.rego
vendored
4
policy/testdata/sad/missing_metadata.rego
vendored
@@ -1,6 +1,6 @@
|
|||||||
package testdata.kubernetes.xyz_100
|
package testdata.kubernetes.xyz_100
|
||||||
|
|
||||||
deny[msg] {
|
deny[msg] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
msg := sprintf("deny %s", [input.metadata.name])
|
msg := sprintf("deny %s", [input.metadata.name])
|
||||||
}
|
}
|
||||||
|
|||||||
10
policy/testdata/sad/missing_metadata_fields.rego
vendored
10
policy/testdata/sad/missing_metadata_fields.rego
vendored
@@ -1,12 +1,12 @@
|
|||||||
package testdata.kubernetes.xyz_100
|
package testdata.kubernetes.xyz_100
|
||||||
|
|
||||||
__rego_metadata__ := {
|
__rego_metadata__ := {
|
||||||
"title": "Bad Deployment",
|
"title": "Bad Deployment",
|
||||||
"version": "v1.0.0",
|
"version": "v1.0.0",
|
||||||
"type": "Kubernetes Security Check",
|
"type": "Kubernetes Security Check",
|
||||||
}
|
}
|
||||||
|
|
||||||
deny[msg] {
|
deny[msg] {
|
||||||
input.kind == "Deployment"
|
input.kind == "Deployment"
|
||||||
msg := sprintf("deny %s", [input.metadata.name])
|
msg := sprintf("deny %s", [input.metadata.name])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ type PolicyMetadata struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PolicyInputOption struct {
|
type PolicyInputOption struct {
|
||||||
Combine bool
|
Combine bool `mapstructure:"combine"`
|
||||||
Selector PolicyInputSelector
|
Selectors []PolicyInputSelector `mapstructure:"selector"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolicyInputSelector struct {
|
type PolicyInputSelector struct {
|
||||||
Types []string
|
Type string `mapstructure:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r MisconfResults) Len() int {
|
func (r MisconfResults) Len() int {
|
||||||
|
|||||||
@@ -43,3 +43,11 @@ func IsGzip(f *bufio.Reader) bool {
|
|||||||
}
|
}
|
||||||
return buf[0] == 0x1F && buf[1] == 0x8B && buf[2] == 0x8
|
return buf[0] == 0x1F && buf[1] == 0x8B && buf[2] == 0x8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Keys(m map[string]struct{}) []string {
|
||||||
|
var keys []string
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user