Skip to content

Commit

Permalink
External messaging (#377)
Browse files Browse the repository at this point in the history
Add a publisher store to enable Archivista to publish information using
different protocols and integrations

* feat: add dapr publisher

this publisher allows users to publish messages (gitoid/dsse payload) to
a dapr HTTP pub/sub.

* feat: add RSTUF publisher

this publisher allows users to integrate Archivista with Repository
Service for TUF, in order to secure the Archivista repository using TUF
metadata signatures.

---------

Signed-off-by: John Kjell <john@testifysec.com>
Signed-off-by: Kairo Araujo <kairo.araujo@testifysec.com>
Co-authored-by: John Kjell <john@testifysec.com>
  • Loading branch information
kairoaraujo and jkjell committed Sep 17, 2024
1 parent d0e318e commit ab2a157
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 9 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ Archivista is configured through environment variables currently.
| ARCHIVISTA_GRAPHQL_WEB_CLIENT_ENABLE | TRUE | Enable GraphiQL, the GraphQL web client |
| ARCHIVISTA_ENABLE_ARTIFACT_STORE | FALSE | Enable Artifact Store Endpoints |
| ARCHIVISTA_ARTIFACT_STORE_CONFIG | /tmp/artifacts/config.yaml | Location of the config describing available artifacts |
| ARCHIVISTA_PUBLISHER | "" | Publisher to use. Options are DAPR, RSTUF. Supports multiple, Comma-separated list of String |
| ARCHIVISTA_PUBLISHER_DAPR_HOST | localhost | Dapr host |
| ARCHIVISTA_PUBLISHER_DAPR_PORT | 3500 | Dapr port |
| ARCHIVISTA_PUBLISHER_DAPR_COMPONENT_NAME | "archivista" | Dapr pubsub component name |
| ARCHIVISTA_PUBLISHER_DAPR_TOPIC | "attestations" | Dapr pubsub topic |
| ARCHIVISTA_PUBLISHER_DAPR_URL | | Dapr full URL |
| ARCHIVISTA_PUBLISHER_RSTUF_HOST | | RSTUF URL |


## Using Archivista

Expand Down
8 changes: 8 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ type Config struct {

EnableArtifactStore bool `default:"FALSE" desc:"*** Enable Artifact Store Endpoints" split_words:"true"`
ArtifactStoreConfig string `default:"/tmp/artifacts/config.yaml" desc:"Location of the config describing available artifacts" split_words:"true"`

Publisher []string `default:"" desc:"Publisher to use. Options are DAPR, RSTUF or empty string for disabled." split_words:"true"`
PublisherDaprHost string `default:"http://127.0.0.1" desc:"Host for Dapr" split_words:"true"`
PublisherDaprPort string `default:"3500" desc:"Port for Dapr" split_words:"true"`
PublisherDaprURL string `default:"" desc:"URL for Dapr" split_words:"true"`
PublisherDaprComponentName string `default:"archivista" desc:"Dapr pubsub component name" split_words:"true"`
PublisherDaprTopic string `default:"attestations" desc:"Dapr pubsub topic" split_words:"true"`
PublisherRstufHost string `default:"http://127.0.0.1" desc:"Host for RSTUF" split_words:"true"`
}

// Process reads config from env
Expand Down
92 changes: 92 additions & 0 deletions pkg/publisherstore/dapr/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2024 The Archivista Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dapr

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/in-toto/archivista/pkg/config"
"github.com/sirupsen/logrus"
)

type DaprHttp struct {
Client *http.Client
Host string
HttpPort string
PubsubComponentName string
PubsubTopic string
Url string
}

type daprPayload struct {
Gitoid string
Payload []byte
}

type Publisher interface {
Publish(ctx context.Context, gitoid string, payload []byte) error
}

func (d *DaprHttp) Publish(ctx context.Context, gitoid string, payload []byte) error {
if d.Client == nil {
d.Client = &http.Client{
Timeout: 15 * time.Second,
}
}

if d.Url == "" {
d.Url = d.Host + ":" + d.HttpPort +
"/v1.0/publish/" + d.PubsubComponentName + "/" + d.PubsubTopic
}

dp := daprPayload{
Gitoid: gitoid,
Payload: payload,
}
// Marshal the message to JSON
msgBytes, err := json.Marshal(dp)
if err != nil {
logrus.Error(err.Error())
return err
}

res, err := d.Client.Post(d.Url, "application/json", bytes.NewReader(msgBytes))
if err != nil {
logrus.Error(err.Error())
return err
}
if res.StatusCode != http.StatusNoContent {
logrus.Printf("failed to publish message: %s", res.Body)
return fmt.Errorf("failed to publish message: %s", res.Body)
}
defer res.Body.Close()

return nil
}

func NewPublisher(config *config.Config) Publisher {
daprPublisher := &DaprHttp{
Host: config.PublisherDaprHost,
HttpPort: config.PublisherDaprPort,
PubsubComponentName: config.PublisherDaprComponentName,
PubsubTopic: config.PublisherDaprTopic,
Url: config.PublisherDaprURL,
}
return daprPublisher
}
47 changes: 47 additions & 0 deletions pkg/publisherstore/publisherstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2024 The Archivista Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package publisherstore

import (
"context"
"strings"

"github.com/in-toto/archivista/pkg/config"
"github.com/in-toto/archivista/pkg/publisherstore/dapr"
"github.com/in-toto/archivista/pkg/publisherstore/rstuf"
"github.com/sirupsen/logrus"
)

type Publisher interface {
Publish(ctx context.Context, gitoid string, payload []byte) error
}

func New(config *config.Config) []Publisher {
var publisherStore []Publisher
for _, pubType := range config.Publisher {
pubType = strings.ToUpper(pubType) // Normalize the input
switch pubType {
case "DAPR":
publisherStore = append(publisherStore, dapr.NewPublisher(config))
logrus.Info("Using publisher: DAPR")

case "RSTUF":
publisherStore = append(publisherStore, rstuf.NewPublisher(config))
logrus.Info("Using publisher: RSTUF")
default:
logrus.Errorf("unsupported publisher type: %s", pubType)
}
}
return publisherStore
}
124 changes: 124 additions & 0 deletions pkg/publisherstore/rstuf/rstuf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2024 The Archivista Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package rstuf

import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"

"github.com/in-toto/archivista/pkg/config"
"github.com/sirupsen/logrus"
)

type RSTUF struct {
Host string
}

type Publisher interface {
Publish(ctx context.Context, gitoid string, payload []byte) error
}

func (r *RSTUF) parseRSTUFPayload(gitoid string, payload []byte) ([]byte, error) {
objHash := sha256.Sum256(payload)
// custom := make(map[string]any)
// custom["gitoid"] = gitoid
artifacts := []Artifact{
{
Path: gitoid,
Info: ArtifactInfo{
Length: len(payload),
Hashes: Hashes{
Sha256: hex.EncodeToString(objHash[:]),
},
// Custom: custom,
},
},
}

artifactPayload := ArtifactPayload{
Artifacts: artifacts,
AddTaskIDToCustom: false,
PublishTargets: true,
}

payloadBytes, err := json.Marshal(artifactPayload)
if err != nil {
return nil, fmt.Errorf("error marshaling payload: %v", err)
}
return payloadBytes, nil
}

func (r *RSTUF) Publish(ctx context.Context, gitoid string, payload []byte) error {
// this publisher allows integration with the RSTUF project to store
// the attestation and policy in the TUF metadata.
// this TUF metadata can be used to build truste when distributing the
// attestations and policies.
// Convert payload to JSON
url := r.Host + "/api/v1/artifacts"

payloadBytes, err := r.parseRSTUFPayload(gitoid, payload)
if err != nil {
return fmt.Errorf("error parsing payload: %v", err)
}

req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes))
if err != nil {
return fmt.Errorf("error creating request: %v", err)
}

req.Header.Set("Content-Type", "application/json")
// Add any additional headers or authentication if needed

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logrus.Errorf("error making request: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusAccepted {
logb, _ := httputil.DumpResponse(resp, true)
logrus.Errorf("error body from RSTUF: %v", string(logb))
return fmt.Errorf("error response from RSTUF: %v", err)
}

// Handle the response as needed
body, err := io.ReadAll(resp.Body)
if err != nil {
logrus.Errorf("error reading response body: %v", err)
}

response := Response{}
err = json.Unmarshal(body, &response)
if err != nil {
logrus.Errorf("error unmarshaling response: %v", err)
}
logrus.Debugf("RSTUF task id: %v", response.Data.TaskId)
// TODO: monitor RSTUF task id for completion
return nil
}

func NewPublisher(config *config.Config) Publisher {
return &RSTUF{
Host: config.PublisherRstufHost,
}
}
51 changes: 51 additions & 0 deletions pkg/publisherstore/rstuf/structs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2024 The Archivista Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package rstuf

// Hashes represents the Hashes structure
type Hashes struct {
Sha256 string `json:"sha256"`
}

// ArtifactInfo represents the ArtifactInfo structure
type ArtifactInfo struct {
Length int `json:"length"`
Hashes Hashes `json:"hashes"`
Custom map[string]any `json:"custom,omitempty"`
}

// Artifact represents the Artifact structure
type Artifact struct {
Path string `json:"path"`
Info ArtifactInfo `json:"info"`
}

// ArtifactPayload represents the payload structure
type ArtifactPayload struct {
Artifacts []Artifact `json:"artifacts"`
AddTaskIDToCustom bool `json:"add_task_id_to_custom"`
PublishTargets bool `json:"publish_targets"`
}

type ArtifactsResponse struct {
Artifacts []string `json:"artifacts"`
TaskId string `json:"task_id"`
LastUpdate string `json:"last_update"`
Message string `json:"message"`
}

type Response struct {
Data ArtifactsResponse `json:"data"`
}
Loading

0 comments on commit ab2a157

Please sign in to comment.