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..3789fd46 --- /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..7e6fb16e --- /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" +} diff --git a/plugins/scorecard/secret_key_test.go b/plugins/scorecard/secret_key_test.go new file mode 100644 index 00000000..dbb7c371 --- /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", + }, + }, + }, + }, + }) +}