{"meta":{"title":"비밀 검사 파트너 프로그램","intro":"서비스 공급자는 GitHub와 협력하여 시크릿 스캐닝을 통해 시크릿 토큰 형식을 보호할 수 있습니다. 시크릿 스캐닝은 시크릿 형식이 실수로 커밋된 경우를 검색하며, 그 결과를 서비스 공급자의 검증 엔드포인트로 보낼 수 있습니다.","product":"보안 및 코드 품질","breadcrumbs":[{"href":"/ko/enterprise-cloud@latest/code-security","title":"보안 및 코드 품질"},{"href":"/ko/enterprise-cloud@latest/code-security/tutorials","title":"Tutorials"},{"href":"/ko/enterprise-cloud@latest/code-security/tutorials/secret-scanning-partner-program","title":"파트너 프로그램"}],"documentType":"article"},"body":"# 비밀 검사 파트너 프로그램\n\n서비스 공급자는 GitHub와 협력하여 시크릿 스캐닝을 통해 시크릿 토큰 형식을 보호할 수 있습니다. 시크릿 스캐닝은 시크릿 형식이 실수로 커밋된 경우를 검색하며, 그 결과를 서비스 공급자의 검증 엔드포인트로 보낼 수 있습니다.\n\nGitHub 실수로 커밋된 자격 증명의 사기성 사용을 방지하기 위해 알려진 비밀 형식에 대한 리포지토리를 검색합니다.\nSecret scanning 는 기본적으로 공용 리포지토리 및 공용 npm 패키지에서 발생합니다. 리포지토리 관리자 및 조직 소유자는 프라이빗 리포지토리에서도 사용하도록 설정할 secret scanning 수 있습니다. 서비스 제공업체로서 GitHub와 협력하여 귀사의 독점 형식이 당사의 secret scanning에 포함되도록 할 수 있습니다.\n\n공개 소스에 비밀 형식의 일치 항목이 있으면 페이로드가 선택한 HTTP 엔드포인트로 전송됩니다.\n\n비밀 형식과 일치하는 항목이 secret scanning에 대해 구성된 프라이빗 리포지토리에서 발견되면, 리포지토리 관리자와 커밋한 사용자가 알림을 받으며 secret scanning에서 GitHub 결과를 보고 관리할 수 있습니다. 자세한 내용은 [비밀 검사 경고 관리](/ko/enterprise-cloud@latest/code-security/how-tos/manage-security-alerts/manage-secret-scanning-alerts)을(를) 참조하세요.\n\n이 문서에서는 서비스 공급자로서 GitHub와 협력하고 secret scanning 파트너 프로그램에 가입하는 방법을 설명합니다.\n\n## secret scanning 프로세스\n\n다음 다이어그램은 공용 리포지토리에 대한 secret scanning 프로세스를 요약하며, 일치하는 항목은 서비스 공급자의 verify 엔드포인트로 전송됩니다. 유사한 프로세스는 npm 레지스트리의 공개 패키지에 노출된 서비스 공급자 토큰을 보냅니다.\n\n![비밀을 검색하고 서비스 공급자의 확인 엔드포인트에 일치 항목을 보내는 프로세스를 보여주는 다이어그램.](/assets/images/help/security/secret-scanning-flow.png)\n\n## secret scanning에서 GitHub 프로그램에 참여하기\n\n1. 프로세스를 시작하려면 문의 GitHub 하세요.\n2. 검색하려는 관련 비밀을 식별하고 해당 비밀을 캡처하는 정규식을 만듭니다. 자세한 내용 및 권장 사항은 아래의 [비밀 식별 및 정규식 만들기](#identify-your-secrets-and-create-regular-expressions)를 참조하세요.\n3. 공개적으로 발견된 시크릿 일치 항목의 경우, GitHub에서 secret scanning 메시지 페이로드가 포함된 웹훅을 수신하는 시크릿 경고 서비스를 만드세요.\n4. 비밀 알림 서비스에서 서명 확인을 구현합니다.\n5. 비밀 알림 서비스에서 비밀 해지 및 사용자 알림을 구현합니다.\n6. 가양성(선택 사항)에 대한 피드백을 제공합니다.\n\n### 프로세스를 시작하려면 연락처로 문의 GitHub 하세요.\n\n등록 프로세스를 시작하려면 <a href=\"mailto:secret-scanning@github.com\"><secret-scanning@github.com></a>으로 이메일을 보내주세요.\n\n프로그램에 대한 secret scanning 세부 정보를 받게 되며 계속하기 전에 참여 조건에 동의 GitHub해야 합니다.\n\n### 비밀 식별 및 정규식 만들기\n\nGitHub에서 비밀 정보를 검색하려면 secret scanning 프로그램에 포함하려는 각 비밀 정보에 대해 다음 정보가 필요합니다:\n\n* 사람이 읽을 수 있으며 비밀 형식에 대한 고유한 이름. 이 정보를 사용하여 나중에 메시지 페이로드에서 `Type` 값을 만들 수 있습니다.\n\n* 비밀 형식을 찾는 정규식. 가능한 정확도를 높이는 것이 좋습니다. 그러면 가양성을 줄이는 데 효과적입니다. 다음은 고품질의 식별 가능한 비밀에 대한 모범 사례입니다.\n\n  * 고유하게 정의된 접두사\n  * 높은 엔트로피 임의 문자열\n  * 32비트 체크섬\n\n  ![비밀을 접두사 및 32비트 체크섬으로 나누어 표시된 스크린샷.](/assets/images/help/security/regular-expression-guidance.png)\n\n* 서비스의 테스트 계정. 이를 통해 비밀의 예제를 생성하고 분석함으로써 가양성을 더욱 줄일 수 있습니다.\n\n* 에서 GitHub메시지를 받는 엔드포인트의 URL입니다. URL은 각 비밀 형식마다 고유할 필요는 없습니다.\n\n이 정보를 <a href=\"mailto:secret-scanning@github.com\"><secret-scanning@github.com></a>에 보냅니다.\n\n### 비밀 경고 서비스 만들기\n\n사용자가 제공한 URL에서 인터넷에 액세스할 수 있는 퍼블릭 HTTP 엔드포인트를 만듭니다. 정규식과 일치하는 항목이 공개 범위에서 발견되면 GitHub가 엔드포인트로 HTTP `POST` 메시지를 보냅니다.\n\n#### 요청 본문 예제\n\n```json\n[\n  {\n    \"token\":\"NMIfyYncKcRALEXAMPLE\",\n    \"type\":\"mycompany_api_token\",\n    \"url\":\"https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt\",\n    \"source\":\"content\"\n  }\n]\n```\n\n메시지 본문은 하나 이상의 객체가 포함된 JSON 배열이며, 각 객체는 단일 비밀 일치 항목을 표시합니다. 엔드포인트는 다수의 일치 항목이 포함된 요청을 시간 초과 없이 처리할 수 있어야 합니다. 각 비밀 일치 항목의 키는 다음과 같습니다.\n\n* **토큰:** 비밀 일치 항목의 값입니다.\n* **형식:** 정규식을 식별하기 위해 제공한 고유한 이름입니다.\n* **URL:** 일치 항목이 발견된 공개 URL입니다(비어 있을 수 있음).\n* **소스:** 토큰이 GitHub에서 발견된 위치입니다.\n\n`source`의 유효한 값 목록은 다음과 같습니다.\n\n* Content\n* Commit\n* Pull\\_request\\_title\n* Pull\\_request\\_description\n* Pull\\_request\\_comment\n* Issue\\_title\n* Issue\\_description\n* issue\\_comment\n* Discussion\\_title\n* Discussion\\_body\n* Discussion\\_comment\n* Commit\\_comment\n* Gist\\_content\n* Gist\\_comment\n* 위키\\_콘텐츠\n* 위키\\_커밋\n* npm\n* 수동 제출\n* 알 수 없음\n\n### 비밀 알림 서비스에서 서명 확인을 구현합니다.\n\n서비스에 대한 HTTP 요청에는 수신한 메시지가 실제로 GitHub에서 온 것이며 악의적이지 않은지 검증하는 데 사용할 것을 강력히 권장하는 헤더도 포함됩니다.\n\n확인할 두 개의 HTTP 헤더는 다음과 같습니다.\n\n* `Github-Public-Key-Identifier`: API에서 사용하는 `key_identifier`\n* `Github-Public-Key-Signature`: 페이로드의 서명\n\nGitHub에서 <https://api.github.com/meta/public_keys/secret_scanning> 시크릿 검색 공개 키를 가져오고 `ECDSA-NIST-P256V1-SHA256` 알고리즘을 사용하여 메시지를 검증할 수 있습니다. 엔드포인트는 여러 개의 `key_identifier`와 공개 키를 제공합니다.\n`Github-Public-Key-Identifier` 값에 따라 사용할 공개 키를 결정할 수 있습니다.\n\n> \\[!NOTE]\n> 위의 공개 키 엔드포인트에 요청을 보내는 경우 속도 제한에 도달할 수 있습니다. 속도 제한에 도달하지 않도록 하려면 아래 샘플에서 제안된 대로 (범위 필요 없음) 또는 personal access token (classic) (자동 공용 리포지토리 읽기 액세스만 필요)를 사용하거나 조건부 요청을 사용할 수 fine-grained personal access token 있습니다. 자세한 내용은 [REST API 사용에 대한 모범 사례](/ko/enterprise-cloud@latest/rest/using-the-rest-api/best-practices-for-using-the-rest-api#use-conditional-requests-if-appropriate)을(를) 참조하세요.\n\n> \\[!NOTE]\n> 서명은 원시 메시지 본문을 사용하여 생성되었습니다. 따라서 메시지를 다시 정렬하거나 간격을 변경하지 않도록 JSON을 구문 분석하고 문자열로 변환하는 대신 서명 유효성 검사에 원시 메시지 본문을 사용하는 것도 중요합니다.\n\n**엔드포인트 유효성 검사를 위해 보낸 HTTP POST 샘플**\n\n```http\nPOST / HTTP/2\nHost: HOST\nAccept: */*\nContent-Length: 104\nContent-Type: application/json\nGithub-Public-Key-Identifier: bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c\nGithub-Public-Key-Signature: MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg==\n\n[{\"source\":\"commit\",\"token\":\"some_token\",\"type\":\"some_type\",\"url\":\"https://example.com/base-repo-url/\"}]\n```\n\n다음 코드 조각은 서명 유효성 검사의 수행 방법을 보여줍니다.\n코드 예제에서는 요청 한도에 걸리지 않도록 생성된 `GITHUB_PRODUCTION_TOKEN`를 사용해 personal access token라는 환경 변수를 설정했다고 가정합니다.\npersonal access token에는 범위/권한이 필요하지 않습니다.\n\n**Go로 작성된 유효성 검사 샘플**\n\n```golang\npackage main\n\nimport (\n  \"crypto/ecdsa\"\n  \"crypto/sha256\"\n  \"crypto/x509\"\n  \"encoding/asn1\"\n  \"encoding/base64\"\n  \"encoding/json\"\n  \"encoding/pem\"\n  \"errors\"\n  \"fmt\"\n  \"math/big\"\n  \"net/http\"\n  \"os\"\n)\n\nfunc main() {\n  payload := `[{\"source\":\"commit\",\"token\":\"some_token\",\"type\":\"some_type\",\"url\":\"https://example.com/base-repo-url/\"}]`\n\n  kID := \"bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c\"\n\n  kSig := \"MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg==\"\n\n  // Fetch the list of GitHub Public Keys\n  req, err := http.NewRequest(\"GET\", \"https://api.github.com/meta/public_keys/secret_scanning\", nil)\n  if err != nil {\n    fmt.Printf(\"Error preparing request: %s\\n\", err)\n    os.Exit(1)\n  }\n\n  if len(os.Getenv(\"GITHUB_PRODUCTION_TOKEN\")) == 0 {\n    fmt.Println(\"Need to define environment variable GITHUB_PRODUCTION_TOKEN\")\n    os.Exit(1)\n  }\n\n  req.Header.Add(\"Authorization\", \"Bearer \"+os.Getenv(\"GITHUB_PRODUCTION_TOKEN\"))\n\n  resp, err := http.DefaultClient.Do(req)\n  if err != nil {\n    fmt.Printf(\"Error requesting GitHub signing keys: %s\\n\", err)\n    os.Exit(2)\n  }\n\n  decoder := json.NewDecoder(resp.Body)\n  var keys GitHubSigningKeys\n  if err := decoder.Decode(&keys); err != nil {\n    fmt.Printf(\"Error decoding GitHub signing key request: %s\\n\", err)\n    os.Exit(3)\n  }\n\n  // Find the Key used to sign our webhook\n  pubKey, err := func() (string, error) {\n    for _, v := range keys.PublicKeys {\n      if v.KeyIdentifier == kID {\n        return v.Key, nil\n\n      }\n    }\n    return \"\", errors.New(\"specified key was not found in GitHub key list\")\n  }()\n\n  if err != nil {\n    fmt.Printf(\"Error finding GitHub signing key: %s\\n\", err)\n    os.Exit(4)\n  }\n\n  // Decode the Public Key\n  block, _ := pem.Decode([]byte(pubKey))\n  if block == nil {\n    fmt.Println(\"Error parsing PEM block with GitHub public key\")\n    os.Exit(5)\n  }\n\n  // Create our ECDSA Public Key\n  key, err := x509.ParsePKIXPublicKey(block.Bytes)\n  if err != nil {\n    fmt.Printf(\"Error parsing DER encoded public key: %s\\n\", err)\n    os.Exit(6)\n  }\n\n  // Because of documentation, we know it's a *ecdsa.PublicKey\n  ecdsaKey, ok := key.(*ecdsa.PublicKey)\n  if !ok {\n    fmt.Println(\"GitHub key was not ECDSA, what are they doing?!\")\n    os.Exit(7)\n  }\n\n  // Parse the Webhook Signature\n  parsedSig := asn1Signature{}\n  asnSig, err := base64.StdEncoding.DecodeString(kSig)\n  if err != nil {\n    fmt.Printf(\"unable to base64 decode signature: %s\\n\", err)\n    os.Exit(8)\n  }\n  rest, err := asn1.Unmarshal(asnSig, &parsedSig)\n  if err != nil || len(rest) != 0 {\n    fmt.Printf(\"Error unmarshalling asn.1 signature: %s\\n\", err)\n    os.Exit(9)\n  }\n\n  // Verify the SHA256 encoded payload against the signature with GitHub's Key\n  digest := sha256.Sum256([]byte(payload))\n  keyOk := ecdsa.Verify(ecdsaKey, digest[:], parsedSig.R, parsedSig.S)\n\n  if keyOk {\n    fmt.Println(\"THE PAYLOAD IS GOOD!!\")\n  } else {\n    fmt.Println(\"the payload is invalid :(\")\n    os.Exit(10)\n  }\n}\n\ntype GitHubSigningKeys struct {\n  PublicKeys []struct {\n    KeyIdentifier string `json:\"key_identifier\"`\n    Key           string `json:\"key\"`\n    IsCurrent     bool   `json:\"is_current\"`\n  } `json:\"public_keys\"`\n}\n\n// asn1Signature is a struct for ASN.1 serializing/parsing signatures.\ntype asn1Signature struct {\n  R *big.Int\n  S *big.Int\n}\n```\n\n**Ruby로 작성된 유효성 검사 샘플**\n\n```ruby\nrequire 'openssl'\nrequire 'net/http'\nrequire 'uri'\nrequire 'json'\nrequire 'base64'\n\npayload = <<-EOL\n[{\"source\":\"commit\",\"token\":\"some_token\",\"type\":\"some_type\",\"url\":\"https://example.com/base-repo-url/\"}]\nEOL\n\npayload = payload\n\nsignature = \"MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg==\"\n\nkey_id = \"bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c\"\n\nurl = URI.parse('https://api.github.com/meta/public_keys/secret_scanning')\n\nraise \"Need to define GITHUB_PRODUCTION_TOKEN environment variable\" unless ENV['GITHUB_PRODUCTION_TOKEN']\nrequest = Net::HTTP::Get.new(url.path)\nrequest['Authorization'] = \"Bearer #{ENV['GITHUB_PRODUCTION_TOKEN']}\"\n\nhttp = Net::HTTP.new(url.host, url.port)\nhttp.use_ssl = (url.scheme == \"https\")\n\nresponse = http.request(request)\n\nparsed_response = JSON.parse(response.body)\n\ncurrent_key_object = parsed_response[\"public_keys\"].find { |key| key[\"key_identifier\"] == key_id }\n\ncurrent_key = current_key_object[\"key\"]\n\nopenssl_key = OpenSSL::PKey::EC.new(current_key)\n\nputs openssl_key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), payload.chomp)\n```\n\n**JavaScript로 작성된 유효성 검사 샘플**\n\n```javascript\nconst crypto = require(\"crypto\");\nconst axios = require(\"axios\");\n\nconst GITHUB_KEYS_URI = \"https://api.github.com/meta/public_keys/secret_scanning\";\n\n/**\n * Verify a payload and signature against a public key\n * @param {String} payload the value to verify\n * @param {String} signature the expected value\n * @param {String} keyID the id of the key used to generated the signature\n * @return {void} throws if the signature is invalid\n */\nconst verify_signature = async (payload, signature, keyID) => {\n  if (typeof payload !== \"string\" || payload.length === 0) {\n    throw new Error(\"Invalid payload\");\n  }\n  if (typeof signature !== \"string\" || signature.length === 0) {\n    throw new Error(\"Invalid signature\");\n  }\n  if (typeof keyID !== \"string\" || keyID.length === 0) {\n    throw new Error(\"Invalid keyID\");\n  }\n\n  const keys = (await axios.get(GITHUB_KEYS_URI)).data;\n  if (!(keys?.public_keys instanceof Array) || keys.length === 0) {\n    throw new Error(\"No public keys found\");\n  }\n\n  const publicKey = keys.public_keys.find((k) => k.key_identifier === keyID) ?? null;\n  if (publicKey === null) {\n    throw new Error(\"No public key found matching key identifier\");\n  }\n\n  const verify = crypto.createVerify(\"SHA256\").update(payload);\n  if (!verify.verify(publicKey.key, Buffer.from(signature, \"base64\"), \"base64\")) {\n    throw new Error(\"Signature does not match payload\");\n  }\n};\n```\n\n### 비밀 알림 서비스에서 비밀 해지 및 사용자 알림을 구현합니다.\n\n공개적으로 발견된 경우 secret scanning 비밀 경고 서비스를 개선하여 노출된 비밀을 해지하고 영향을 받는 사용자에게 알릴 수 있습니다. 시크릿 경고 서비스에서 이를 구현하는 방법은 귀하에게 달려 있지만, GitHub가 메시지를 보내 알려 주는 모든 시크릿은 이미 공개되었고 유출된 것으로 간주하는 것이 좋습니다.\n\n### 가양성에 대한 피드백 제공\n\n파트너 응답에서 검색된 개별 비밀의 유효성에 대한 피드백을 수집합니다. 참여하려면 <a href=\"mailto:secret-scanning@github.com\"><secret-scanning@github.com></a>으로 이메일을 보내주세요.\n\n귀하에게 비밀을 보고할 때 토큰, 형식 식별자 및 커밋 URL을 포함하고 있는 각 요소가 포함된 JSON 배열을 보냅니다. 귀하가 피드백을 보낼 때 검색된 토큰이 실제 또는 거짓 자격 증명인지에 대한 정보를 같이 보냅니다. 다음과 같은 형식의 피드백을 수락합니다.\n\n원시 토큰을 보낼 수 있습니다.\n\n```json\n[\n  {\n    \"token_raw\": \"The raw token\",\n    \"token_type\": \"ACompany_API_token\",\n    \"label\": \"true_positive\"\n  }\n]\n```\n\n또한 SHA-256을 사용하여 원시 토큰의 단방향 암호화 해시를 수행한 후 해시된 형식으로 토큰을 제공할 수도 있습니다.\n\n```json\n[\n  {\n    \"token_hash\": \"The SHA-256 hashed form of the raw token\",\n    \"token_type\": \"ACompany_API_token\",\n    \"label\": \"false_positive\"\n  }\n]\n```\n\n몇 가지 중요 사항:\n\n* 토큰의 원시 형식(“token\\_raw”) 또는 해시된 양식(“token\\_hash”)만 보내야 하지만 둘 다 보내지는 않습니다.\n* 원시 토큰의 해시된 형식의 경우 SHA-256만 사용하여 다른 해시 알고리즘이 아닌 토큰을 해시할 수 있습니다.\n* 레이블은 토큰이 진양성(“true\\_positive”) 또는 가양성(“false\\_positive”)인지 여부를 나타냅니다. 이러한 소문자 리터럴 문자열 두 개만 허용됩니다.\n\n> \\[!NOTE]\n> 가양성에 대한 데이터를 제공하는 파트너에게는 더 길어진(30초) 요청 시간 제한이 적용됩니다. 30초보다 더 긴 시간 제한이 필요한 경우 <a href=\"mailto:secret-scanning@github.com\"><secret-scanning@github.com></a>으로 이메일을 보내주세요."}