Add ecr image scan (#4166)
* Add ecr.start_image_scan * Add ecr.describe_image_scan_findings
This commit is contained in:
parent
a5eb46962d
commit
b8405b39b5
@ -16,6 +16,16 @@ class LifecyclePolicyNotFoundException(JsonRESTError):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LimitExceededException(JsonRESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
error_type=__class__.__name__,
|
||||||
|
message=("The scan quota per image has been exceeded. Wait and try again."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegistryPolicyNotFoundException(JsonRESTError):
|
class RegistryPolicyNotFoundException(JsonRESTError):
|
||||||
code = 400
|
code = 400
|
||||||
|
|
||||||
@ -101,3 +111,17 @@ class InvalidParameterException(JsonRESTError):
|
|||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
super().__init__(error_type=__class__.__name__, message=message)
|
super().__init__(error_type=__class__.__name__, message=message)
|
||||||
|
|
||||||
|
|
||||||
|
class ScanNotFoundException(JsonRESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self, image_id, repository_name, registry_id):
|
||||||
|
super().__init__(
|
||||||
|
error_type=__class__.__name__,
|
||||||
|
message=(
|
||||||
|
f"Image scan does not exist for the image with '{image_id}' "
|
||||||
|
f"in the repository with name '{repository_name}' "
|
||||||
|
f"in the registry with id '{registry_id}'"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -7,7 +7,7 @@ import uuid
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import random
|
from random import random
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
from botocore.exceptions import ParamValidationError
|
from botocore.exceptions import ParamValidationError
|
||||||
|
|
||||||
@ -23,6 +23,8 @@ from moto.ecr.exceptions import (
|
|||||||
RepositoryPolicyNotFoundException,
|
RepositoryPolicyNotFoundException,
|
||||||
LifecyclePolicyNotFoundException,
|
LifecyclePolicyNotFoundException,
|
||||||
RegistryPolicyNotFoundException,
|
RegistryPolicyNotFoundException,
|
||||||
|
LimitExceededException,
|
||||||
|
ScanNotFoundException,
|
||||||
)
|
)
|
||||||
from moto.ecr.policy_validation import EcrLifecyclePolicyValidator
|
from moto.ecr.policy_validation import EcrLifecyclePolicyValidator
|
||||||
from moto.iam.exceptions import MalformedPolicyDocument
|
from moto.iam.exceptions import MalformedPolicyDocument
|
||||||
@ -87,7 +89,7 @@ class Repository(BaseObject, CloudFormationModel):
|
|||||||
)
|
)
|
||||||
self.policy = None
|
self.policy = None
|
||||||
self.lifecycle_policy = None
|
self.lifecycle_policy = None
|
||||||
self.images = []
|
self.images: List[Image] = []
|
||||||
|
|
||||||
def _determine_encryption_config(self, encryption_config):
|
def _determine_encryption_config(self, encryption_config):
|
||||||
if not encryption_config:
|
if not encryption_config:
|
||||||
@ -98,6 +100,31 @@ class Repository(BaseObject, CloudFormationModel):
|
|||||||
] = f"arn:aws:kms:{self.region_name}:{ACCOUNT_ID}:key/{uuid.uuid4()}"
|
] = f"arn:aws:kms:{self.region_name}:{ACCOUNT_ID}:key/{uuid.uuid4()}"
|
||||||
return encryption_config
|
return encryption_config
|
||||||
|
|
||||||
|
def _get_image(self, image_tag, image_digest):
|
||||||
|
# you can either search for one or both
|
||||||
|
image = next(
|
||||||
|
(
|
||||||
|
i
|
||||||
|
for i in self.images
|
||||||
|
if (not image_tag or image_tag in i.image_tags)
|
||||||
|
and (not image_digest or image_digest == i.get_image_digest())
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not image:
|
||||||
|
image_id_rep = "{{imageDigest:'{0}', imageTag:'{1}'}}".format(
|
||||||
|
image_digest or "null", image_tag or "null"
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ImageNotFoundException(
|
||||||
|
image_id=image_id_rep,
|
||||||
|
repository_name=self.name,
|
||||||
|
registry_id=self.registry_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def physical_resource_id(self):
|
def physical_resource_id(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -210,6 +237,7 @@ class Image(BaseObject):
|
|||||||
self.registry_id = registry_id
|
self.registry_id = registry_id
|
||||||
self.image_digest = digest
|
self.image_digest = digest
|
||||||
self.image_pushed_at = str(datetime.utcnow().isoformat())
|
self.image_pushed_at = str(datetime.utcnow().isoformat())
|
||||||
|
self.last_scan = None
|
||||||
|
|
||||||
def _create_digest(self):
|
def _create_digest(self):
|
||||||
image_contents = "docker_image{0}".format(int(random() * 10 ** 6))
|
image_contents = "docker_image{0}".format(int(random() * 10 ** 6))
|
||||||
@ -407,38 +435,15 @@ class ECRBackend(BaseBackend):
|
|||||||
return images
|
return images
|
||||||
|
|
||||||
def describe_images(self, repository_name, registry_id=None, image_ids=None):
|
def describe_images(self, repository_name, registry_id=None, image_ids=None):
|
||||||
|
repository = self._get_repository(repository_name, registry_id)
|
||||||
if repository_name in self.repositories:
|
|
||||||
repository = self.repositories[repository_name]
|
|
||||||
else:
|
|
||||||
raise RepositoryNotFoundException(
|
|
||||||
repository_name, registry_id or DEFAULT_REGISTRY_ID
|
|
||||||
)
|
|
||||||
|
|
||||||
if image_ids:
|
if image_ids:
|
||||||
response = set()
|
response = set(
|
||||||
for image_id in image_ids:
|
repository._get_image(
|
||||||
found = False
|
image_id.get("imageTag"), image_id.get("imageDigest")
|
||||||
for image in repository.images:
|
)
|
||||||
if (
|
for image_id in image_ids
|
||||||
"imageDigest" in image_id
|
)
|
||||||
and image.get_image_digest() == image_id["imageDigest"]
|
|
||||||
) or (
|
|
||||||
"imageTag" in image_id
|
|
||||||
and image_id["imageTag"] in image.image_tags
|
|
||||||
):
|
|
||||||
found = True
|
|
||||||
response.add(image)
|
|
||||||
if not found:
|
|
||||||
image_id_representation = "{imageDigest:'%s', imageTag:'%s'}" % (
|
|
||||||
image_id.get("imageDigest", "null"),
|
|
||||||
image_id.get("imageTag", "null"),
|
|
||||||
)
|
|
||||||
raise ImageNotFoundException(
|
|
||||||
image_id=image_id_representation,
|
|
||||||
repository_name=repository_name,
|
|
||||||
registry_id=registry_id or DEFAULT_REGISTRY_ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
response = []
|
response = []
|
||||||
@ -816,6 +821,81 @@ class ECRBackend(BaseBackend):
|
|||||||
"policyText": policy,
|
"policyText": policy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def start_image_scan(self, registry_id, repository_name, image_id):
|
||||||
|
repo = self._get_repository(repository_name, registry_id)
|
||||||
|
|
||||||
|
image = repo._get_image(image_id.get("imageTag"), image_id.get("imageDigest"))
|
||||||
|
|
||||||
|
# scanning an image is only allowed once per day
|
||||||
|
if image.last_scan and image.last_scan.date() == datetime.today().date():
|
||||||
|
raise LimitExceededException()
|
||||||
|
|
||||||
|
image.last_scan = datetime.today()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"registryId": repo.registry_id,
|
||||||
|
"repositoryName": repository_name,
|
||||||
|
"imageId": {
|
||||||
|
"imageDigest": image.image_digest,
|
||||||
|
"imageTag": image.image_tag,
|
||||||
|
},
|
||||||
|
"imageScanStatus": {"status": "IN_PROGRESS"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def describe_image_scan_findings(self, registry_id, repository_name, image_id):
|
||||||
|
repo = self._get_repository(repository_name, registry_id)
|
||||||
|
|
||||||
|
image = repo._get_image(image_id.get("imageTag"), image_id.get("imageDigest"))
|
||||||
|
|
||||||
|
if not image.last_scan:
|
||||||
|
image_id_rep = "{{imageDigest:'{0}', imageTag:'{1}'}}".format(
|
||||||
|
image_id.get("imageDigest") or "null",
|
||||||
|
image_id.get("imageTag") or "null",
|
||||||
|
)
|
||||||
|
raise ScanNotFoundException(
|
||||||
|
image_id=image_id_rep,
|
||||||
|
repository_name=repository_name,
|
||||||
|
registry_id=repo.registry_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"registryId": repo.registry_id,
|
||||||
|
"repositoryName": repository_name,
|
||||||
|
"imageId": {
|
||||||
|
"imageDigest": image.image_digest,
|
||||||
|
"imageTag": image.image_tag,
|
||||||
|
},
|
||||||
|
"imageScanStatus": {
|
||||||
|
"status": "COMPLETE",
|
||||||
|
"description": "The scan was completed successfully.",
|
||||||
|
},
|
||||||
|
"imageScanFindings": {
|
||||||
|
"imageScanCompletedAt": iso_8601_datetime_without_milliseconds(
|
||||||
|
image.last_scan
|
||||||
|
),
|
||||||
|
"vulnerabilitySourceUpdatedAt": iso_8601_datetime_without_milliseconds(
|
||||||
|
datetime.utcnow()
|
||||||
|
),
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"name": "CVE-9999-9999",
|
||||||
|
"uri": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-9999-9999",
|
||||||
|
"severity": "HIGH",
|
||||||
|
"attributes": [
|
||||||
|
{"key": "package_version", "value": "9.9.9"},
|
||||||
|
{"key": "package_name", "value": "moto_fake"},
|
||||||
|
{
|
||||||
|
"key": "CVSS2_VECTOR",
|
||||||
|
"value": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
|
||||||
|
},
|
||||||
|
{"key": "CVSS2_SCORE", "value": "7.5"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"findingSeverityCounts": {"HIGH": 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ecr_backends = {}
|
ecr_backends = {}
|
||||||
for region, ec2_backend in ec2_backends.items():
|
for region, ec2_backend in ec2_backends.items():
|
||||||
|
@ -271,3 +271,29 @@ class ECRResponse(BaseResponse):
|
|||||||
|
|
||||||
def delete_registry_policy(self):
|
def delete_registry_policy(self):
|
||||||
return json.dumps(self.ecr_backend.delete_registry_policy())
|
return json.dumps(self.ecr_backend.delete_registry_policy())
|
||||||
|
|
||||||
|
def start_image_scan(self):
|
||||||
|
registry_id = self._get_param("registryId")
|
||||||
|
repository_name = self._get_param("repositoryName")
|
||||||
|
image_id = self._get_param("imageId")
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
self.ecr_backend.start_image_scan(
|
||||||
|
registry_id=registry_id,
|
||||||
|
repository_name=repository_name,
|
||||||
|
image_id=image_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def describe_image_scan_findings(self):
|
||||||
|
registry_id = self._get_param("registryId")
|
||||||
|
repository_name = self._get_param("repositoryName")
|
||||||
|
image_id = self._get_param("imageId")
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
self.ecr_backend.describe_image_scan_findings(
|
||||||
|
registry_id=registry_id,
|
||||||
|
repository_name=repository_name,
|
||||||
|
image_id=image_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -651,24 +651,24 @@ def test_describe_image_that_doesnt_exist():
|
|||||||
|
|
||||||
error_msg1 = re.compile(
|
error_msg1 = re.compile(
|
||||||
r".*The image with imageId {imageDigest:'null', imageTag:'testtag'} does not exist within "
|
r".*The image with imageId {imageDigest:'null', imageTag:'testtag'} does not exist within "
|
||||||
r"the repository with name 'test_repository' in the registry with id '123'.*",
|
r"the repository with name 'test_repository' in the registry with id '123456789012'.*",
|
||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
client.describe_images.when.called_with(
|
client.describe_images.when.called_with(
|
||||||
repositoryName="test_repository",
|
repositoryName="test_repository",
|
||||||
imageIds=[{"imageTag": "testtag"}],
|
imageIds=[{"imageTag": "testtag"}],
|
||||||
registryId="123",
|
registryId=ACCOUNT_ID,
|
||||||
).should.throw(client.exceptions.ImageNotFoundException, error_msg1)
|
).should.throw(client.exceptions.ImageNotFoundException, error_msg1)
|
||||||
|
|
||||||
error_msg2 = re.compile(
|
error_msg2 = re.compile(
|
||||||
r".*The repository with name 'repo-that-doesnt-exist' does not exist in the registry with id '123'.*",
|
r".*The repository with name 'repo-that-doesnt-exist' does not exist in the registry with id '123456789012'.*",
|
||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
client.describe_images.when.called_with(
|
client.describe_images.when.called_with(
|
||||||
repositoryName="repo-that-doesnt-exist",
|
repositoryName="repo-that-doesnt-exist",
|
||||||
imageIds=[{"imageTag": "testtag"}],
|
imageIds=[{"imageTag": "testtag"}],
|
||||||
registryId="123",
|
registryId=ACCOUNT_ID,
|
||||||
).should.throw(ClientError, error_msg2)
|
).should.throw(ClientError, error_msg2)
|
||||||
|
|
||||||
|
|
||||||
@ -2113,3 +2113,272 @@ def test_delete_registry_policy_error_policy_not_exists():
|
|||||||
ex.response["Error"]["Message"].should.equal(
|
ex.response["Error"]["Message"].should.equal(
|
||||||
f"Registry policy does not exist in the registry with id '{ACCOUNT_ID}'"
|
f"Registry policy does not exist in the registry with id '{ACCOUNT_ID}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_start_image_scan():
|
||||||
|
# given
|
||||||
|
client = boto3.client("ecr", region_name="eu-central-1")
|
||||||
|
repo_name = "test-repo"
|
||||||
|
client.create_repository(repositoryName=repo_name)
|
||||||
|
image_tag = "latest"
|
||||||
|
image_digest = client.put_image(
|
||||||
|
repositoryName=repo_name,
|
||||||
|
imageManifest=json.dumps(_create_image_manifest()),
|
||||||
|
imageTag="latest",
|
||||||
|
)["image"]["imageId"]["imageDigest"]
|
||||||
|
|
||||||
|
# when
|
||||||
|
response = client.start_image_scan(
|
||||||
|
repositoryName=repo_name, imageId={"imageTag": image_tag}
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
response["registryId"].should.equal(ACCOUNT_ID)
|
||||||
|
response["repositoryName"].should.equal(repo_name)
|
||||||
|
response["imageId"].should.equal(
|
||||||
|
{"imageDigest": image_digest, "imageTag": image_tag}
|
||||||
|
)
|
||||||
|
response["imageScanStatus"].should.equal({"status": "IN_PROGRESS"})
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_start_image_scan_error_repo_not_exists():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("ecr", region_name=region_name)
|
||||||
|
repo_name = "not-exists"
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.start_image_scan(
|
||||||
|
repositoryName=repo_name, imageId={"imageTag": "latest"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("StartImageScan")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("RepositoryNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
f"The repository with name '{repo_name}' does not exist "
|
||||||
|
f"in the registry with id '{ACCOUNT_ID}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_start_image_scan_error_image_not_exists():
|
||||||
|
# given
|
||||||
|
client = boto3.client("ecr", region_name="eu-central-1")
|
||||||
|
repo_name = "test-repo"
|
||||||
|
client.create_repository(repositoryName=repo_name)
|
||||||
|
image_tag = "not-exists"
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.start_image_scan(
|
||||||
|
repositoryName=repo_name, imageId={"imageTag": image_tag}
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("StartImageScan")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("ImageNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
f"The image with imageId {{imageDigest:'null', imageTag:'{image_tag}'}} does not exist "
|
||||||
|
f"within the repository with name '{repo_name}' "
|
||||||
|
f"in the registry with id '{ACCOUNT_ID}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_start_image_scan_error_image_tag_digest_mismatch():
|
||||||
|
# given
|
||||||
|
client = boto3.client("ecr", region_name="eu-central-1")
|
||||||
|
repo_name = "test-repo"
|
||||||
|
client.create_repository(repositoryName=repo_name)
|
||||||
|
image_digest = client.put_image(
|
||||||
|
repositoryName=repo_name,
|
||||||
|
imageManifest=json.dumps(_create_image_manifest()),
|
||||||
|
imageTag="latest",
|
||||||
|
)["image"]["imageId"]["imageDigest"]
|
||||||
|
image_tag = "not-latest"
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.start_image_scan(
|
||||||
|
repositoryName=repo_name,
|
||||||
|
imageId={"imageTag": image_tag, "imageDigest": image_digest},
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("StartImageScan")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("ImageNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
f"The image with imageId {{imageDigest:'{image_digest}', imageTag:'{image_tag}'}} does not exist "
|
||||||
|
f"within the repository with name '{repo_name}' "
|
||||||
|
f"in the registry with id '{ACCOUNT_ID}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_start_image_scan_error_daily_limit():
|
||||||
|
# given
|
||||||
|
client = boto3.client("ecr", region_name="eu-central-1")
|
||||||
|
repo_name = "test-repo"
|
||||||
|
client.create_repository(repositoryName=repo_name)
|
||||||
|
image_tag = "latest"
|
||||||
|
image_digest = client.put_image(
|
||||||
|
repositoryName=repo_name,
|
||||||
|
imageManifest=json.dumps(_create_image_manifest()),
|
||||||
|
imageTag="latest",
|
||||||
|
)["image"]["imageId"]["imageDigest"]
|
||||||
|
client.start_image_scan(repositoryName=repo_name, imageId={"imageTag": image_tag})
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.start_image_scan(
|
||||||
|
repositoryName=repo_name, imageId={"imageTag": image_tag}
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("StartImageScan")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("LimitExceededException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"The scan quota per image has been exceeded. Wait and try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_describe_image_scan_findings():
|
||||||
|
# given
|
||||||
|
client = boto3.client("ecr", region_name="eu-central-1")
|
||||||
|
repo_name = "test-repo"
|
||||||
|
client.create_repository(repositoryName=repo_name)
|
||||||
|
image_tag = "latest"
|
||||||
|
image_digest = client.put_image(
|
||||||
|
repositoryName=repo_name,
|
||||||
|
imageManifest=json.dumps(_create_image_manifest()),
|
||||||
|
imageTag="latest",
|
||||||
|
)["image"]["imageId"]["imageDigest"]
|
||||||
|
client.start_image_scan(repositoryName=repo_name, imageId={"imageTag": image_tag})
|
||||||
|
|
||||||
|
# when
|
||||||
|
response = client.describe_image_scan_findings(
|
||||||
|
repositoryName=repo_name, imageId={"imageTag": image_tag}
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
response["registryId"].should.equal(ACCOUNT_ID)
|
||||||
|
response["repositoryName"].should.equal(repo_name)
|
||||||
|
response["imageId"].should.equal(
|
||||||
|
{"imageDigest": image_digest, "imageTag": image_tag}
|
||||||
|
)
|
||||||
|
response["imageScanStatus"].should.equal(
|
||||||
|
{"status": "COMPLETE", "description": "The scan was completed successfully."}
|
||||||
|
)
|
||||||
|
scan_findings = response["imageScanFindings"]
|
||||||
|
scan_findings["imageScanCompletedAt"].should.be.a(datetime)
|
||||||
|
scan_findings["vulnerabilitySourceUpdatedAt"].should.be.a(datetime)
|
||||||
|
scan_findings["findings"].should.equal(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "CVE-9999-9999",
|
||||||
|
"uri": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-9999-9999",
|
||||||
|
"severity": "HIGH",
|
||||||
|
"attributes": [
|
||||||
|
{"key": "package_version", "value": "9.9.9"},
|
||||||
|
{"key": "package_name", "value": "moto_fake"},
|
||||||
|
{"key": "CVSS2_VECTOR", "value": "AV:N/AC:L/Au:N/C:P/I:P/A:P",},
|
||||||
|
{"key": "CVSS2_SCORE", "value": "7.5"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
scan_findings["findingSeverityCounts"].should.equal({"HIGH": 1})
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_describe_image_scan_findings_error_repo_not_exists():
|
||||||
|
# given
|
||||||
|
region_name = "eu-central-1"
|
||||||
|
client = boto3.client("ecr", region_name=region_name)
|
||||||
|
repo_name = "not-exists"
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.describe_image_scan_findings(
|
||||||
|
repositoryName=repo_name, imageId={"imageTag": "latest"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("DescribeImageScanFindings")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("RepositoryNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
f"The repository with name '{repo_name}' does not exist "
|
||||||
|
f"in the registry with id '{ACCOUNT_ID}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_describe_image_scan_findings_error_image_not_exists():
|
||||||
|
# given
|
||||||
|
client = boto3.client("ecr", region_name="eu-central-1")
|
||||||
|
repo_name = "test-repo"
|
||||||
|
client.create_repository(repositoryName=repo_name)
|
||||||
|
image_tag = "not-exists"
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.describe_image_scan_findings(
|
||||||
|
repositoryName=repo_name, imageId={"imageTag": image_tag}
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("DescribeImageScanFindings")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("ImageNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
f"The image with imageId {{imageDigest:'null', imageTag:'{image_tag}'}} does not exist "
|
||||||
|
f"within the repository with name '{repo_name}' "
|
||||||
|
f"in the registry with id '{ACCOUNT_ID}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecr
|
||||||
|
def test_describe_image_scan_findings_error_scan_not_exists():
|
||||||
|
# given
|
||||||
|
client = boto3.client("ecr", region_name="eu-central-1")
|
||||||
|
repo_name = "test-repo"
|
||||||
|
client.create_repository(repositoryName=repo_name)
|
||||||
|
image_tag = "latest"
|
||||||
|
client.put_image(
|
||||||
|
repositoryName=repo_name,
|
||||||
|
imageManifest=json.dumps(_create_image_manifest()),
|
||||||
|
imageTag=image_tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(ClientError) as e:
|
||||||
|
client.describe_image_scan_findings(
|
||||||
|
repositoryName=repo_name, imageId={"imageTag": image_tag}
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
ex = e.value
|
||||||
|
ex.operation_name.should.equal("DescribeImageScanFindings")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("ScanNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
f"Image scan does not exist for the image with '{{imageDigest:'null', imageTag:'{image_tag}'}}' "
|
||||||
|
f"in the repository with name '{repo_name}' "
|
||||||
|
f"in the registry with id '{ACCOUNT_ID}'"
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user