Skip to content

Commit

Permalink
Send image urls (#956)
Browse files Browse the repository at this point in the history
* test banding images (rough code sketch)

* add log

* add fe log for testing

* send xkcd comic instead of url (since it's not working properly)

* add logs

* add content store name log

* change logic to directly check CS name

* add log to ensure prodstore works

* fix prod store

* check via name rather than type

* ensure s3 logic goes through

* check for correct STore type

* remove xkcd comic as dummy string

* add a few TODOS

* create correct Type for url

* s3 and other images work now

* add logs for testing

* cleanup small things

* send string instead of object

* fix non-img media

* add log to check s3 results

* add another log

* see if url exists

* add additoinal logging

* check to see if reader is working

* put in demo image if

* remove logging for reader

* ensure only images are sent out of band

* change reader to be io.Reader

* send evidence preview to determine issue

* change logs to prevent failure?

* remove other logic to ensure reponse works

* use exact same logic

* test using terminal reader to see what values are

* clean up / new logs

* create separate endpoint

* Revert "create separate endpoint"

This reverts commit b7152d4.

* make sure I'm not emptying the buffer

* try new reader method

* try longer buffer

* set length to be lenght of url

* add log to see how long url is

* try to get it working again

* use simpler reader

* try ReadAll

* try generic read with loop

* create url endpoint

* add some logs

* add additional logging

* correct url type

* add content type

* add log to see specific url

* relog url

* add additonal logs

* check type

* parse json

* parse further down

* fix json error message

* follow dto conventions

* TODO cleanup

* additional cleanup

* add back content headers

* try another reader method

* only send url stream for images

* add logs

* clean up extraneous endpoint

* correct boolean

* add logs

* remove logs

* remove TODO

* ensure only images get sendUrl

* revert index page

* fix spacing

* fix spacing agian

* revert accidental changes

* fix spacing

* add lazy load component

* lowering s3 url validity to 30 minutes

---------

Co-authored-by: John Kennedy <[email protected]>
  • Loading branch information
Tyler Noblett and jkennedyvz authored Nov 7, 2023
1 parent 4b1c725 commit 5d49edf
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 8 deletions.
17 changes: 17 additions & 0 deletions backend/contentstore/s3store.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package contentstore

import (
"io"
"time"

"github.com/ashirt-ops/ashirt-server/backend"
"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -70,6 +71,22 @@ func (s *S3Store) Read(key string) (io.Reader, error) {
return res.Body, nil
}

func (s *S3Store) SendURL(key string) (*string, error) {
contentType := "image/jpeg"
req, _ := s.s3Client.GetObjectRequest(&s3.GetObjectInput{
Bucket: aws.String(s.bucketName),
Key: aws.String(key),
ResponseContentType: aws.String(contentType),
})

url, err := req.Presign(time.Minute * 30)
if err != nil {
return nil, backend.WrapError("Unable to get presigned URL", err)
}

return &url, nil
}

// Delete removes files in in your OS's temp directory
func (s *S3Store) Delete(key string) error {
_, err := s.s3Client.DeleteObject(&s3.DeleteObjectInput{
Expand Down
1 change: 1 addition & 0 deletions backend/dtos/dtos.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Evidence struct {
Operator User `json:"operator"`
Tags []Tag `json:"tags"`
ContentType string `json:"contentType"`
SendUrl bool `json:"sendUrl"`
}

type EvidenceMetadata struct {
Expand Down
11 changes: 9 additions & 2 deletions backend/server/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package server

import (
"bytes"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -479,7 +480,7 @@ func bindWebRoutes(r chi.Router, db *database.Connection, contentStore contentst
if dr.Error != nil {
return nil, dr.Error
}
return services.ListEvidenceForOperation(r.Context(), db, i)
return services.ListEvidenceForOperation(r.Context(), db, contentStore, i)
}))

route(r, "GET", "/operations/{operation_slug}/evidence/creators", jsonHandler(func(r *http.Request) (interface{}, error) {
Expand Down Expand Up @@ -519,7 +520,13 @@ func bindWebRoutes(r chi.Router, db *database.Connection, contentStore contentst
if err != nil {
return nil, backend.WrapError("Unable to read evidence", err)
}

if s3Store, ok := contentStore.(*contentstore.S3Store); ok && evidence.ContentType == "image" {
url, err := services.SendUrl(r.Context(), db, s3Store, i)
if err != nil {
return nil, backend.WrapError("Unable get s3 URL", err)
}
return bytes.NewReader([]byte(*url)), nil
}
if i.LoadPreview {
return evidence.Preview, nil
}
Expand Down
30 changes: 29 additions & 1 deletion backend/services/evidence.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ func ListEvidenceForFinding(ctx context.Context, db *database.Connection, i List

// ListEvidenceForOperation retrieves all evidence for a particular operation id matching a particular
// set of filters (e.g. tag:some_tag)
func ListEvidenceForOperation(ctx context.Context, db *database.Connection, i ListEvidenceForOperationInput) ([]*dtos.Evidence, error) {
func ListEvidenceForOperation(ctx context.Context, db *database.Connection, contentStore contentstore.Store, i ListEvidenceForOperationInput) ([]*dtos.Evidence, error) {
operation, err := lookupOperation(db, i.OperationSlug)
if err != nil {
return nil, backend.WrapError("Unable to list evidence for an operation", backend.UnauthorizedReadErr(err))
Expand Down Expand Up @@ -316,25 +316,53 @@ func ListEvidenceForOperation(ctx context.Context, db *database.Connection, i Li
}

evidenceDTO := make([]*dtos.Evidence, len(evidence))

usingS3 := false
if _, ok := contentStore.(*contentstore.S3Store); ok {
usingS3 = true
}

for idx, evi := range evidence {
tags, ok := tagsByEvidenceID[evi.ID]

if !ok {
tags = []dtos.Tag{}
}

sendUrl := false
if usingS3 && evi.ContentType == "image" {
sendUrl = true
}

evidenceDTO[idx] = &dtos.Evidence{
UUID: evi.UUID,
Description: evi.Description,
Operator: dtos.User{FirstName: evi.FirstName, LastName: evi.LastName, Slug: evi.Slug},
OccurredAt: evi.OccurredAt,
ContentType: evi.ContentType,
Tags: tags,
SendUrl: sendUrl,
}
}
return evidenceDTO, nil
}

func SendUrl(ctx context.Context, db *database.Connection, contentStore *contentstore.S3Store, i ReadEvidenceInput) (*string, error) {
operation, evidence, err := lookupOperationEvidence(db, i.OperationSlug, i.EvidenceUUID)
if err != nil {
return nil, backend.WrapError("Unable to read evidence", backend.UnauthorizedReadErr(err))
}
if err := policy.Require(middleware.Policy(ctx), policy.CanReadOperation{OperationID: operation.ID}); err != nil {
return nil, backend.WrapError("Unwilling to read evidence", backend.UnauthorizedReadErr(err))
}
str, err := contentStore.SendURL(evidence.FullImageKey)
if err != nil {
return nil, backend.WrapError("Unable to get image URL", backend.ServerErr(err))
}

return str, nil

}
func ReadEvidence(ctx context.Context, db *database.Connection, contentStore contentstore.Store, i ReadEvidenceInput) (*ReadEvidenceOutput, error) {
operation, evidence, err := lookupOperationEvidence(db, i.OperationSlug, i.EvidenceUUID)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion backend/services/evidence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ func TestListEvidenceForFinding(t *testing.T) {
func TestListEvidenceForOperation(t *testing.T) {
RunResettableDBTest(t, func(db *database.Connection, _ TestSeedData) {
ctx := contextForUser(UserRon, db)
cs, _ := contentstore.NewMemStore()

masterOp := OpChamberOfSecrets
allEvidence := getFullEvidenceByOperationID(t, db, masterOp.ID)
Expand All @@ -206,7 +207,7 @@ func TestListEvidenceForOperation(t *testing.T) {
Filters: helpers.TimelineFilters{},
}

foundEvidence, err := services.ListEvidenceForOperation(ctx, db, input)
foundEvidence, err := services.ListEvidenceForOperation(ctx, db, cs, input)
require.NoError(t, err)
require.Equal(t, len(foundEvidence), len(allEvidence))
validateEvidenceSets(t, toRealEvidenceList(foundEvidence), allEvidence, validateEvidence)
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/evidence_chooser/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const EvidenceRow = (props: {
evidenceUuid={props.evidence.uuid}
contentType={props.evidence.contentType}
onClick={(e) => { e.stopPropagation(); setLightboxOpen(true)} }
useS3Url={props.evidence.sendUrl}
fitToContainer
viewHint="small"
interactionHint="inactive"
Expand All @@ -55,6 +56,7 @@ const EvidenceRow = (props: {
operationSlug={props.operationSlug}
evidenceUuid={props.evidence.uuid}
contentType={props.evidence.contentType}
useS3Url={props.evidence.sendUrl}
viewHint="large"
interactionHint="active"
/>
Expand Down
18 changes: 15 additions & 3 deletions frontend/src/components/evidence_preview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { SupportedEvidenceType, CodeBlock, EvidenceViewHint, InteractionHint } f
import { getEvidenceAsCodeblock, getEvidenceAsString, updateEvidence } from 'src/services/evidence'
import { useWiredData } from 'src/helpers'
import ErrorDisplay from 'src/components/error_display'
import LazyLoadComponent from 'src/components/lazy_load_component'


import TerminalPlayer from 'src/components/terminal_player'

Expand Down Expand Up @@ -40,6 +42,7 @@ export default (props: {
interactionHint?: InteractionHint,
className?: string,
fitToContainer?: boolean,
useS3Url: boolean,
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void,
}) => {
const Component = getComponent(props.contentType)
Expand All @@ -55,7 +58,7 @@ export default (props: {

return (
<div className={className} onClick={props.onClick}>
<Component {...props} />
<LazyLoadComponent><Component {...props} /></LazyLoadComponent>
</div>
)
}
Expand All @@ -65,6 +68,7 @@ type EvidenceProps = {
evidenceUuid: string,
viewHint?: EvidenceViewHint,
interactionHint?: InteractionHint,
useS3Url: boolean
}

const EvidenceCodeblock = (props: EvidenceProps) => {
Expand All @@ -77,8 +81,16 @@ const EvidenceCodeblock = (props: EvidenceProps) => {
}

const EvidenceImage = (props: EvidenceProps) => {
const fullUrl = `/web/operations/${props.operationSlug}/evidence/${props.evidenceUuid}/media`
return <img src={fullUrl} />
if (props.useS3Url) {
const wiredUrl = useWiredData<string>(React.useCallback(() => getEvidenceAsString({
operationSlug: props.operationSlug,
evidenceUuid: props.evidenceUuid,
}), [props.operationSlug, props.evidenceUuid]))
return wiredUrl.render(url => <img src={url} />)
} else {
const fullUrl = `/web/operations/${props.operationSlug}/evidence/${props.evidenceUuid}/media`
return <img src={fullUrl} />
}
}

const EvidenceEvent = (_props: EvidenceProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ const uuidToBasicEvidence = (uuid: string): Evidence => ({
occurredAt: new Date(),
tags: [],
contentType: 'none',
sendUrl: false
})

const ChooseEvidenceModal = (props: {
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/components/lazy_load_component/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2023, Yahoo Inc.
// Licensed under the terms of the MIT. See LICENSE file in project root for terms.

import React, { useState, useEffect, useRef } from 'react';

export default (props: { children: React.ReactNode }) => {
const [isVisible, setIsVisible] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ root: null, rootMargin: '0px', threshold: 0.1 }
);

if (containerRef.current) {
observer.observe(containerRef.current);
}

return () => {
if (containerRef.current) {
observer.unobserve(containerRef.current);
}
};
}, []);

return (
<div ref={containerRef}>
{isVisible ? props.children : null}
</div>
);
}
2 changes: 2 additions & 0 deletions frontend/src/components/timeline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export default (props: {
operationSlug={props.operationSlug}
evidenceUuid={activeEvidence.uuid}
contentType={activeEvidence.contentType}
useS3Url={activeEvidence.sendUrl}
viewHint="large"
interactionHint="active"
/>
Expand Down Expand Up @@ -158,6 +159,7 @@ const TimelineRow = (props: {
operationSlug={props.operationSlug}
evidenceUuid={props.evidence.uuid}
contentType={props.evidence.contentType}
useS3Url={props.evidence.sendUrl}
viewHint="medium"
interactionHint="inactive"
/>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/global_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export type Evidence = {
occurredAt: Date,
tags: Array<Tag>,
contentType: SupportedEvidenceType
sendUrl: boolean,
}

export type ExportedEvidence = Omit<Evidence, 'tags' | 'uuid'> & {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/operation_edit/batch_run_worker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export const BatchRunWorker = (props: {
occurredAt: new Date(),
tags: [],
metadata: [],
contentType: 'image'
contentType: 'image',
sendUrl: false
}))
)}
{...modalProps}
Expand Down

0 comments on commit 5d49edf

Please sign in to comment.