Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MLPAB-1472: Update virus scanning results on evidence in dynamodb #779

Merged
merged 32 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
65ad134
handle document-scanned events
acsauk Oct 17, 2023
4e9afd3
drop unused struct
acsauk Oct 17, 2023
8a457a1
Merge branch 'main' into MLPAB-1472
acsauk Oct 18, 2023
34291f8
make clear we are getting a document
acsauk Oct 18, 2023
fc82dde
add object tags added event commands
acsauk Oct 18, 2023
a4c2609
ignore all but app main.go in air (as we dont rebuild them)
acsauk Oct 18, 2023
5fe0827
handle objectTagsAdded events
acsauk Oct 18, 2023
8a72e06
Merge branch 'main' into MLPAB-1472
acsauk Oct 18, 2023
cc06e4e
cov bump
acsauk Oct 18, 2023
d128bb7
tidy
acsauk Oct 19, 2023
f350c67
Merge branch 'main' into MLPAB-1472
acsauk Oct 19, 2023
7621d50
Merge branch 'main' into MLPAB-1472
acsauk Oct 19, 2023
7974d9f
simplify tag checking, bring Evidence funcs in line with other collec…
acsauk Oct 20, 2023
58e91f4
Merge branch 'MLPAB-1472' of github.com:ministryofjustice/opg-moderni…
acsauk Oct 20, 2023
8facfd6
Merge branch 'main' into MLPAB-1472
acsauk Oct 20, 2023
14fdc92
Merge branch 'main' into MLPAB-1472
acsauk Oct 20, 2023
12a2a22
allow s3:ObjectTagging:Put events to be seen in EventBridge
acsauk Oct 20, 2023
f58d861
Merge branch 'MLPAB-1472' of github.com:ministryofjustice/opg-moderni…
acsauk Oct 20, 2023
7a25379
actually add event to to the real resource
acsauk Oct 20, 2023
a9d6c76
debug
acsauk Oct 20, 2023
b91790a
revert terraform changes
acsauk Oct 23, 2023
86134f3
Merge branch 'main' into MLPAB-1472
acsauk Oct 23, 2023
0360bca
Merge branch 'main' into MLPAB-1472
acsauk Oct 23, 2023
f41af23
see the event
acsauk Oct 23, 2023
90ad9bf
handle S3Events
acsauk Oct 23, 2023
f6a70d0
update tests
acsauk Oct 23, 2023
bdf265d
Merge branch 'main' into MLPAB-1472
acsauk Oct 23, 2023
c1eb0ee
set bucket name
acsauk Oct 23, 2023
4c73416
allow getting tags for objects rather than bucket
acsauk Oct 23, 2023
60cc1c9
use bucket name
acsauk Oct 23, 2023
49d8c45
Merge branch 'main' into MLPAB-1472
acsauk Oct 24, 2023
98bdfd8
Merge branch 'main' into MLPAB-1472
acsauk Oct 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root = "."
tmp_dir = "tmp"

[build]
exclude_dir = ["cypress", "docs", "tmp", "web/assets", "web/static", "node_modules", "scripts", "terraform"]
exclude_dir = ["cypress", "docs", "tmp", "web/assets", "web/static", "node_modules", "scripts", "terraform", "cmd/event-received", "cmd/enumerator", "cmd/event-mock-notify", "cmd/mock.onelogin", "cmd/mock-os-api"]
cmd = "cd cmd/mlpa && go build -ldflags='-X main.Tag=${TAG}' -gcflags='all=-N -l' -o /tmp/mlpa ."
full_bin = "pkill -9 'dlv|mlpa'; sleep 0.1; dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 /tmp/mlpa"
include_ext = ["go", "gohtml", "json"]
36 changes: 24 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,37 @@ run-structurizr-export:
scan-lpas: ##@app dumps all entries in the lpas dynamodb table
docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb scan --table-name lpas

get-lpa: ##@app dumps all entries in the lpas dynamodb table that are related to the LPA id supplied e.g. get-lpa ID=abc-123
get-lpa: ##@app dumps all entries in the lpas dynamodb table that are related to the LPA id supplied e.g. get-lpa id=abc-123
docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb \
query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(ID)"}}'
query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(id)"}}'

get-evidence: ##@app dumps all fee evidence in the lpas dynamodb table that are related to the LPA id supplied e.g. get-evidence ID=abc-123
get-evidence: ##@app dumps all fee evidence in the lpas dynamodb table that are related to the LPA id supplied e.g. get-evidence id=abc-123
docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb \
query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(ID)"}}' --projection-expression "EvidenceKeys"
query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(id)"}}' --projection-expression "Evidence"

emit-evidence-received: ##@app emits an evidence-received event with the given UID e.g. emit-evidence-received UID=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"evidence-received","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(UID)"}}'
emit-evidence-received: ##@app emits an evidence-received event with the given UID e.g. emit-evidence-received uid=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"evidence-received","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(uid)"}}'

emit-fee-approved: ##@app emits a fee-approved event with the given UID e.g. emit-fee-approved UID=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"fee-approved","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(UID)"}}'
emit-fee-approved: ##@app emits a fee-approved event with the given UID e.g. emit-fee-approved uid=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"fee-approved","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(uid)"}}'

emit-fee-denied: ##@app emits a fee-denied event with the given UID e.g. emit-fee-denied UID=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"fee-denied","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(UID)"}}'
emit-fee-denied: ##@app emits a fee-denied event with the given UID e.g. emit-fee-denied uid=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"fee-denied","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(uid)"}}'

emit-more-evidence-required: ##@app emits a more-evidence-required event with the given UID e.g. emit-more-evidence-required UID=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"more-evidence-required","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(UID)"}}'
emit-more-evidence-required: ##@app emits a more-evidence-required event with the given UID e.g. emit-more-evidence-required uid=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"more-evidence-required","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(uid)"}}'

emit-object-tags-added-with-virus: ##@app emits a Object Tags Added event with the given S3 key e.g. emit-object-tags-added-with-virus key=doc/key. Also ensures a tag with virus-scan-status exists on an existing object set to infected
docker compose -f docker/docker-compose.yml exec localstack awslocal s3api \
put-object-tagging --bucket evidence --key $(key) --tagging '{"TagSet": [{ "Key": "virus-scan-status", "Value": "infected" }]}'

curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"Object Tags Added","source":"aws.s3","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"object":{"key":"$(key)"}}}'

emit-object-tags-added-without-virus: ##@app emits a Object Tags Added event with the given S3 key e.g. emit-object-tags-added-with-virus key=doc/key. Also ensures a tag with virus-scan-status exists on an existing object set to ok
docker compose -f docker/docker-compose.yml exec localstack awslocal s3api \
put-object-tagging --bucket evidence --key $(key) --tagging '{"TagSet": [{ "Key": "virus-scan-status", "Value": "ok" }]}'

curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"Object Tags Added","source":"aws.s3","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"object":{"key":"$(key)"}}}'

logs: ##@app tails logs for all containers running
docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml logs -f
193 changes: 140 additions & 53 deletions cmd/event-received/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,37 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/ministryofjustice/opg-modernising-lpa/internal/actor"
"github.com/ministryofjustice/opg-modernising-lpa/internal/app"
"github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo"
"github.com/ministryofjustice/opg-modernising-lpa/internal/localize"
"github.com/ministryofjustice/opg-modernising-lpa/internal/notify"
"github.com/ministryofjustice/opg-modernising-lpa/internal/page"
"github.com/ministryofjustice/opg-modernising-lpa/internal/random"
"github.com/ministryofjustice/opg-modernising-lpa/internal/s3"
"github.com/ministryofjustice/opg-modernising-lpa/internal/secrets"
)

const (
virusFound = "infected"
evidenceReceivedEventName = "evidence-received"
feeApprovedEventName = "fee-approved"
feeDeniedEventName = "fee-denied"
moreEvidenceRequiredEventName = "more-evidence-required"
objectTagsAddedEventName = "ObjectTagging:Put"
)

type uidEvent struct {
UID string `json:"uid"`
}
Expand All @@ -34,17 +45,36 @@ type dynamodbClient interface {
Put(ctx context.Context, v interface{}) error
}

//go:generate mockery --testonly --inpackage --name s3Client --structname mockS3Client
type s3Client interface {
GetObjectTags(ctx context.Context, key string) ([]types.Tag, error)
}

//go:generate mockery --testonly --inpackage --name shareCodeSender --structname mockShareCodeSender
type shareCodeSender interface {
SendCertificateProvider(context.Context, notify.Template, page.AppData, bool, *page.Lpa) error
}

func Handler(ctx context.Context, event events.CloudWatchEvent) error {
type Event struct {
events.S3Event
events.CloudWatchEvent
}

func (e Event) isS3Event() bool {
return len(e.Records) > 0
}

func (e Event) isCloudWatchEvent() bool {
return e.Source == "aws.cloudwatch"
}

func Handler(ctx context.Context, event Event) error {
tableName := os.Getenv("LPAS_TABLE")
notifyIsProduction := os.Getenv("GOVUK_NOTIFY_IS_PRODUCTION") == "1"
appPublicURL := os.Getenv("APP_PUBLIC_URL")
awsBaseURL := os.Getenv("AWS_BASE_URL")
notifyBaseURL := os.Getenv("GOVUK_NOTIFY_BASE_URL")
evidenceBucketName := os.Getenv("UPLOADS_S3_BUCKET_NAME")

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
Expand All @@ -61,6 +91,8 @@ func Handler(ctx context.Context, event events.CloudWatchEvent) error {
})
}

s3Client := s3.NewClient(cfg, evidenceBucketName)

dynamoClient, err := dynamo.NewClient(cfg, tableName)
if err != nil {
return fmt.Errorf("failed to create dynamodb client: %w", err)
Expand All @@ -86,37 +118,46 @@ func Handler(ctx context.Context, event events.CloudWatchEvent) error {
shareCodeSender := page.NewShareCodeSender(app.NewShareCodeStore(dynamoClient), notifyClient, appPublicURL, random.String)
now := time.Now

switch event.DetailType {
case "evidence-received":
return handleEvidenceReceived(ctx, dynamoClient, event)
case "fee-approved":
return handleFeeApproved(ctx, dynamoClient, event, shareCodeSender, appData, now)
case "more-evidence-required":
return handleMoreEvidenceRequired(ctx, dynamoClient, event, now)
case "fee-denied":
return handleFeeDenied(ctx, dynamoClient, event, now)
default:
return fmt.Errorf("unknown event received: %s", event.DetailType)
if event.isS3Event() {
return handleObjectTagsAdded(ctx, dynamoClient, event.S3Event, now, s3Client)
}

if event.isCloudWatchEvent() {
switch event.DetailType {
case evidenceReceivedEventName:
return handleEvidenceReceived(ctx, dynamoClient, event.CloudWatchEvent)
case feeApprovedEventName:
return handleFeeApproved(ctx, dynamoClient, event.CloudWatchEvent, shareCodeSender, appData, now)
case moreEvidenceRequiredEventName:
return handleMoreEvidenceRequired(ctx, dynamoClient, event.CloudWatchEvent, now)
case feeDeniedEventName:
return handleFeeDenied(ctx, dynamoClient, event.CloudWatchEvent, now)
default:
return fmt.Errorf("unknown event received: %s", event.DetailType)
}
}

eJson, _ := json.Marshal(event)
return fmt.Errorf("unknown event type received: %s", string(eJson))
}

func handleEvidenceReceived(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent) error {
var v uidEvent
if err := json.Unmarshal(event.Detail, &v); err != nil {
return fmt.Errorf("failed to unmarshal 'evidence-received' detail: %w", err)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", evidenceReceivedEventName, err)
}

var key dynamo.Key
if err := client.OneByUID(ctx, v.UID, &key); err != nil {
return fmt.Errorf("failed to resolve uid for 'evidence-received': %w", err)
return fmt.Errorf("failed to resolve uid for '%s': %w", evidenceReceivedEventName, err)
}

if key.PK == "" {
return errors.New("PK missing from LPA in response to 'evidence-received'")
return fmt.Errorf("PK missing from LPA in response to '%s'", evidenceReceivedEventName)
}

if err := client.Put(ctx, map[string]string{"PK": key.PK, "SK": "#EVIDENCE_RECEIVED"}); err != nil {
return fmt.Errorf("failed to persist evidence received for 'evidence-received': %w", err)
return fmt.Errorf("failed to persist evidence received for '%s': %w", evidenceReceivedEventName, err)
}

return nil
Expand All @@ -125,28 +166,23 @@ func handleEvidenceReceived(ctx context.Context, client dynamodbClient, event ev
func handleFeeApproved(ctx context.Context, dynamoClient dynamodbClient, event events.CloudWatchEvent, shareCodeSender shareCodeSender, appData page.AppData, now func() time.Time) error {
var v uidEvent
if err := json.Unmarshal(event.Detail, &v); err != nil {
return fmt.Errorf("failed to unmarshal 'fee-approved' detail: %w", err)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", feeApprovedEventName, err)
}

var key dynamo.Key
if err := dynamoClient.OneByUID(ctx, v.UID, &key); err != nil {
return fmt.Errorf("failed to resolve uid for 'fee-approved': %w", err)
}

var lpa page.Lpa
if err := dynamoClient.One(ctx, key.PK, key.SK, &lpa); err != nil {
return fmt.Errorf("failed to get LPA for 'fee-approved': %w", err)
lpa, err := getLpaByUID(ctx, dynamoClient, v.UID, feeApprovedEventName)
if err != nil {
return err
}

lpa.Tasks.PayForLpa = actor.PaymentTaskCompleted
lpa.UpdatedAt = now()

if err := dynamoClient.Put(ctx, lpa); err != nil {
return fmt.Errorf("failed to update LPA task status for 'fee-approved': %w", err)
return fmt.Errorf("failed to update LPA task status for '%s': %w", feeApprovedEventName, err)
}

if err := shareCodeSender.SendCertificateProvider(ctx, notify.CertificateProviderInviteEmail, appData, false, &lpa); err != nil {
return fmt.Errorf("failed to send share code to certificate provider for 'fee-approved': %w", err)
return fmt.Errorf("failed to send share code to certificate provider for '%s': %w", feeApprovedEventName, err)
}

return nil
Expand All @@ -155,28 +191,19 @@ func handleFeeApproved(ctx context.Context, dynamoClient dynamodbClient, event e
func handleMoreEvidenceRequired(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error {
var v uidEvent
if err := json.Unmarshal(event.Detail, &v); err != nil {
return fmt.Errorf("failed to unmarshal 'more-evidence-required' detail: %w", err)
}

var key dynamo.Key
if err := client.OneByUID(ctx, v.UID, &key); err != nil {
return fmt.Errorf("failed to resolve uid for 'more-evidence-required': %w", err)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", moreEvidenceRequiredEventName, err)
}

if key.PK == "" {
return errors.New("PK missing from LPA in response to 'more-evidence-required'")
}

var lpa page.Lpa
if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil {
return fmt.Errorf("failed to get LPA for 'more-evidence-required': %w", err)
lpa, err := getLpaByUID(ctx, client, v.UID, moreEvidenceRequiredEventName)
if err != nil {
return err
}

lpa.Tasks.PayForLpa = actor.PaymentTaskMoreEvidenceRequired
lpa.UpdatedAt = now()

if err := client.Put(ctx, lpa); err != nil {
return fmt.Errorf("failed to update LPA task status for 'more-evidence-required': %w", err)
return fmt.Errorf("failed to update LPA task status for '%s': %w", moreEvidenceRequiredEventName, err)
}

return nil
Expand All @@ -185,33 +212,93 @@ func handleMoreEvidenceRequired(ctx context.Context, client dynamodbClient, even
func handleFeeDenied(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error {
var v uidEvent
if err := json.Unmarshal(event.Detail, &v); err != nil {
return fmt.Errorf("failed to unmarshal 'fee-denied' detail: %w", err)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", feeDeniedEventName, err)
}

var key dynamo.Key
if err := client.OneByUID(ctx, v.UID, &key); err != nil {
return fmt.Errorf("failed to resolve uid for 'fee-denied': %w", err)
lpa, err := getLpaByUID(ctx, client, v.UID, feeDeniedEventName)
if err != nil {
return err
}

if key.PK == "" {
return errors.New("PK missing from LPA in response to 'fee-denied'")
lpa.Tasks.PayForLpa = actor.PaymentTaskDenied
lpa.UpdatedAt = now()

if err := client.Put(ctx, lpa); err != nil {
return fmt.Errorf("failed to update LPA task status for '%s': %w", feeDeniedEventName, err)
}

var lpa page.Lpa
if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil {
return fmt.Errorf("failed to get LPA for 'fee-denied': %w", err)
return nil
}

func handleObjectTagsAdded(ctx context.Context, client dynamodbClient, event events.S3Event, now func() time.Time, s3Client s3Client) error {
objectKey := event.Records[0].S3.Object.Key
if objectKey == "" {
return fmt.Errorf("object key missing in event in '%s'", objectTagsAddedEventName)
}

lpa.Tasks.PayForLpa = actor.PaymentTaskDenied
tags, err := s3Client.GetObjectTags(ctx, objectKey)
if err != nil {
return fmt.Errorf("failed to get tags for object in '%s': %w", objectTagsAddedEventName, err)
}

hasScannedTag := false
hasVirus := false

for _, tag := range tags {
if *tag.Key == "virus-scan-status" {
hasScannedTag = true
hasVirus = *tag.Value == virusFound
break
}
}

if !hasScannedTag {
return nil
}

uid := strings.Split(objectKey, "/")

lpa, err := getLpaByUID(ctx, client, uid[0], objectTagsAddedEventName)
if err != nil {
return err
}

document := lpa.Evidence.Get(objectKey)
if document.Key == "" {
return fmt.Errorf("LPA did not contain a document with key %s for '%s'", objectKey, objectTagsAddedEventName)
}

document.Scanned = now()
document.VirusDetected = hasVirus

lpa.Evidence.Put(document)
lpa.UpdatedAt = now()

if err := client.Put(ctx, lpa); err != nil {
return fmt.Errorf("failed to update LPA task status for 'fee-denied': %w", err)
return fmt.Errorf("failed to update LPA for '%s': %w", objectTagsAddedEventName, err)
}

return nil
}

func getLpaByUID(ctx context.Context, client dynamodbClient, uid, eventName string) (page.Lpa, error) {
var key dynamo.Key
if err := client.OneByUID(ctx, uid, &key); err != nil {
return page.Lpa{}, fmt.Errorf("failed to resolve uid for '%s': %w", eventName, err)
}

if key.PK == "" {
return page.Lpa{}, fmt.Errorf("PK missing from LPA in response to '%s'", eventName)
}

var lpa page.Lpa
if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil {
return page.Lpa{}, fmt.Errorf("failed to get LPA for '%s': %w", eventName, err)
}

return lpa, nil
}

func main() {
lambda.Start(Handler)
}
Loading