From 060f0e03ca144029cba122826f31fa0475d08081 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:55:45 -0500 Subject: [PATCH 1/2] feat: add plugin support for OpenSSF Scorecard CLI --- plugins/scorecard/plugin.go | 22 +++++++ plugins/scorecard/scorecard.go | 25 ++++++++ plugins/scorecard/secret_key.go | 83 +++++++++++++++++++++++++ plugins/scorecard/secret_key_test.go | 92 ++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 plugins/scorecard/plugin.go create mode 100644 plugins/scorecard/scorecard.go create mode 100644 plugins/scorecard/secret_key.go create mode 100644 plugins/scorecard/secret_key_test.go diff --git a/plugins/scorecard/plugin.go b/plugins/scorecard/plugin.go new file mode 100644 index 00000000..b07b9de8 --- /dev/null +++ b/plugins/scorecard/plugin.go @@ -0,0 +1,22 @@ +package scorecard + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "scorecard", + Platform: schema.PlatformInfo{ + Name: "OpenSSF Scorecard", + Homepage: sdk.URL("https://github.com/ossf/scorecard"), + }, + Credentials: []schema.CredentialType{ + SecretKey(), + }, + Executables: []schema.Executable{ + OpenSSFScorecardCLI(), + }, + } +} diff --git a/plugins/scorecard/scorecard.go b/plugins/scorecard/scorecard.go new file mode 100644 index 00000000..5be55cc4 --- /dev/null +++ b/plugins/scorecard/scorecard.go @@ -0,0 +1,25 @@ +package scorecard + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func OpenSSFScorecardCLI() schema.Executable { + return schema.Executable{ + Name: "OpenSSF Scorecard CLI", + Runs: []string{"scorecard"}, + DocsURL: sdk.URL("https://github.com/ossf/scorecard"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.SecretKey, + }, + }, + } +} diff --git a/plugins/scorecard/secret_key.go b/plugins/scorecard/secret_key.go new file mode 100644 index 00000000..8f4c604f --- /dev/null +++ b/plugins/scorecard/secret_key.go @@ -0,0 +1,83 @@ +package scorecard + +import ( + "context" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func SecretKey() schema.CredentialType { + return schema.CredentialType{ + Name: credname.SecretKey, + DocsURL: sdk.URL("https://github.com/ossf/scorecard?tab=readme-ov-file#authentication"), + ManagementURL: sdk.URL("https://github.com/settings/installations"), + Fields: []schema.CredentialField{ + { + Name: fieldname.Key, + MarkdownDescription: "RSA private key used to authenticate GitHub App for OpenSSF Scorecard. This should be a PEM-formatted private key.", + Secret: true, + }, + { + Name: "App ID", + MarkdownDescription: "GitHub App ID for authentication. This is a numeric ID.", + Secret: false, + Composition: &schema.ValueComposition{ + Charset: schema.Charset{ + Digits: true, + }, + }, + }, + { + Name: "Installation ID", + MarkdownDescription: "GitHub App Installation ID for the target repository or organization. This is a numeric ID and is required.", + Secret: false, + Composition: &schema.ValueComposition{ + Charset: schema.Charset{ + Digits: true, + }, + }, + }, + }, + DefaultProvisioner: scorecardProvisioner{}, + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + ), + } +} + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "GITHUB_APP_KEY_PATH": fieldname.Key, + "GITHUB_APP_ID": "App ID", + "GITHUB_APP_INSTALLATION_ID": "Installation ID", +} + +type scorecardProvisioner struct{} + +func (p scorecardProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { + // Provision the private key as a file + if key, ok := in.ItemFields[fieldname.Key]; ok { + keyPath := in.FromTempDir("github-app-key.pem") + out.AddSecretFile(keyPath, []byte(key)) + out.AddEnvVar("GITHUB_APP_KEY_PATH", keyPath) + } + + // Provision App ID and Installation ID as environment variables + if appID, ok := in.ItemFields["App ID"]; ok && appID != "" { + out.AddEnvVar("GITHUB_APP_ID", appID) + } + if installationID, ok := in.ItemFields["Installation ID"]; ok && installationID != "" { + out.AddEnvVar("GITHUB_APP_INSTALLATION_ID", installationID) + } +} + +func (p scorecardProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) { + // Files get deleted automatically by 1Password CLI and environment variables get wiped when process exits +} + +func (p scorecardProvisioner) Description() string { + return "Provision GitHub App private key as file and IDs as environment variables for OpenSSF Scorecard" +} \ No newline at end of file diff --git a/plugins/scorecard/secret_key_test.go b/plugins/scorecard/secret_key_test.go new file mode 100644 index 00000000..2446b102 --- /dev/null +++ b/plugins/scorecard/secret_key_test.go @@ -0,0 +1,92 @@ +package scorecard + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func generateTestPrivateKey(t *testing.T) string { + // Generate a test RSA private key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Failed to generate test private key: %v", err) + } + + // Encode to PEM format + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + }) + + return string(privateKeyPEM) +} + +func TestSecretKeyProvisioner(t *testing.T) { + testPrivateKey := generateTestPrivateKey(t) + + plugintest.TestProvisioner(t, SecretKey().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "default": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Key: testPrivateKey, + "App ID": "123456", + "Installation ID": "7890123", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "GITHUB_APP_ID": "123456", + "GITHUB_APP_INSTALLATION_ID": "7890123", + "GITHUB_APP_KEY_PATH": "/tmp/github-app-key.pem", + }, + Files: map[string]sdk.OutputFile{ + "/tmp/github-app-key.pem": {Contents: []byte(testPrivateKey)}, + }, + }, + }, + "partial fields": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Key: testPrivateKey, + "App ID": "123456", + // Installation ID is missing to test handling of partial fields + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "GITHUB_APP_ID": "123456", + "GITHUB_APP_KEY_PATH": "/tmp/github-app-key.pem", + // GITHUB_APP_INSTALLATION_ID should not be set when field is missing + }, + Files: map[string]sdk.OutputFile{ + "/tmp/github-app-key.pem": {Contents: []byte(testPrivateKey)}, + }, + }, + }, + }) +} + +func TestSecretKeyImporter(t *testing.T) { + plugintest.TestImporter(t, SecretKey().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ + "GITHUB_APP_KEY_PATH": "/path/to/key.pem", + "GITHUB_APP_ID": "123456", + "GITHUB_APP_INSTALLATION_ID": "7890123", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.Key: "/path/to/key.pem", + "App ID": "123456", + "Installation ID": "7890123", + }, + }, + }, + }, + }) +} \ No newline at end of file From 29c592d22c05c369d94430f376f145e008bc8a34 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:50:19 -0500 Subject: [PATCH 2/2] run gofmt --- plugins/scorecard/scorecard.go | 6 +++--- plugins/scorecard/secret_key.go | 6 +++--- plugins/scorecard/secret_key_test.go | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/scorecard/scorecard.go b/plugins/scorecard/scorecard.go index 5be55cc4..3789fd46 100644 --- a/plugins/scorecard/scorecard.go +++ b/plugins/scorecard/scorecard.go @@ -9,9 +9,9 @@ import ( func OpenSSFScorecardCLI() schema.Executable { return schema.Executable{ - Name: "OpenSSF Scorecard CLI", - Runs: []string{"scorecard"}, - DocsURL: sdk.URL("https://github.com/ossf/scorecard"), + Name: "OpenSSF Scorecard CLI", + Runs: []string{"scorecard"}, + DocsURL: sdk.URL("https://github.com/ossf/scorecard"), NeedsAuth: needsauth.IfAll( needsauth.NotForHelpOrVersion(), needsauth.NotWithoutArgs(), diff --git a/plugins/scorecard/secret_key.go b/plugins/scorecard/secret_key.go index 8f4c604f..7e6fb16e 100644 --- a/plugins/scorecard/secret_key.go +++ b/plugins/scorecard/secret_key.go @@ -50,8 +50,8 @@ func SecretKey() schema.CredentialType { } var defaultEnvVarMapping = map[string]sdk.FieldName{ - "GITHUB_APP_KEY_PATH": fieldname.Key, - "GITHUB_APP_ID": "App ID", + "GITHUB_APP_KEY_PATH": fieldname.Key, + "GITHUB_APP_ID": "App ID", "GITHUB_APP_INSTALLATION_ID": "Installation ID", } @@ -80,4 +80,4 @@ func (p scorecardProvisioner) Deprovision(ctx context.Context, in sdk.Deprovisio func (p scorecardProvisioner) Description() string { return "Provision GitHub App private key as file and IDs as environment variables for OpenSSF Scorecard" -} \ No newline at end of file +} diff --git a/plugins/scorecard/secret_key_test.go b/plugins/scorecard/secret_key_test.go index 2446b102..dbb7c371 100644 --- a/plugins/scorecard/secret_key_test.go +++ b/plugins/scorecard/secret_key_test.go @@ -6,12 +6,12 @@ import ( "crypto/x509" "encoding/pem" "testing" - + "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/plugintest" "github.com/1Password/shell-plugins/sdk/schema/fieldname" ) - + func generateTestPrivateKey(t *testing.T) string { // Generate a test RSA private key privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -35,8 +35,8 @@ func TestSecretKeyProvisioner(t *testing.T) { plugintest.TestProvisioner(t, SecretKey().DefaultProvisioner, map[string]plugintest.ProvisionCase{ "default": { ItemFields: map[sdk.FieldName]string{ - fieldname.Key: testPrivateKey, - "App ID": "123456", + fieldname.Key: testPrivateKey, + "App ID": "123456", "Installation ID": "7890123", }, ExpectedOutput: sdk.ProvisionOutput{ @@ -53,7 +53,7 @@ func TestSecretKeyProvisioner(t *testing.T) { "partial fields": { ItemFields: map[sdk.FieldName]string{ fieldname.Key: testPrivateKey, - "App ID": "123456", + "App ID": "123456", // Installation ID is missing to test handling of partial fields }, ExpectedOutput: sdk.ProvisionOutput{ @@ -74,9 +74,9 @@ func TestSecretKeyImporter(t *testing.T) { plugintest.TestImporter(t, SecretKey().Importer, map[string]plugintest.ImportCase{ "environment": { Environment: map[string]string{ - "GITHUB_APP_KEY_PATH": "/path/to/key.pem", - "GITHUB_APP_ID": "123456", - "GITHUB_APP_INSTALLATION_ID": "7890123", + "GITHUB_APP_KEY_PATH": "/path/to/key.pem", + "GITHUB_APP_ID": "123456", + "GITHUB_APP_INSTALLATION_ID": "7890123", }, ExpectedCandidates: []sdk.ImportCandidate{ { @@ -89,4 +89,4 @@ func TestSecretKeyImporter(t *testing.T) { }, }, }) -} \ No newline at end of file +}