Blog Cover Image

Inspire you to have New thinking, Walk out your unique Road.

[後端技術研究] Black Duck APIs 和 Fortify On Demand APIs 怎麼使用? (透過Python Requests或Postman)

Posted onDec 4, 2023

公司有一項冗長繁瑣的程式資安審核流程使用 Black Duck 和 Fortify On Demand Scans,為了能達成自動化兩個掃描功能,我們研究了這兩項掃描服務的 APIs,並在此文章紀錄我們的研究成果。

研究 Black Duck(BD)Fortify On Demand(FoD) 掃描服務,是由於公司希望能將這兩項掃描服務自動化,在經過 3 週左右的研究,我成功的運用 APIs 達成我們所需的功能,將研究結果記錄於此篇。

請注意,此篇紀錄的 API endpoints 僅有達成我們功能所需的 APIs,並非所有。

因為我當初在研究時,發現網路上幾乎沒有其他人分享怎麼使用這兩個掃描服務的 APIs,所以如果你幸運需要研究如何自動化或者如何使用 APIs,希望這篇文章能幫助到你。

原則上,兩項掃描服務的 APIs 文件都寫得還算詳細,但還是會遇到一些小問題,所以我會把那些遇到的坑寫出來。

大綱

以下是本篇文章介紹的 APIs 大綱

| 掃描服務 | 功能 | 描述 | API | | ----------------- | ------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | | Fortify On Demand | Oauth Authentication | 取得授權的 Oauth token,才能使用這個 token call 其他 APIs | https://api.ams.fortify.com/oauth/token | | | Get Application | 取得所有的 applications | https://api.ams.fortify.com/api/v3/application | | | Get Release (with filter) | 取得 release 或隸屬於指定的 application 下的 release,包含講解 filter 查找功能 | https://api.ams.fortify.com/api/v3/release 或 https://api.ams.fortify.com/api/v3/application/<application_id>/release | | Black Duck | Authentication | 取得用來 call 其他 API 的 bearer token | https://<host_name>/api/tokens/authenticate | | | Query project | 找到指定的 projct (含 filter 過濾) | https://<host_name>/api/projects | | | Get version | 取得指定的 project 下所有的版本資訊 | https://<host_name>/api/projects/<project_id>/versions | | | Generate Version Details Report | 對於指定的 project 及版本產出 Version Details 報告 | https://<host_name>/api/versions/<version_id>/reports | | | Download Version Details Report | 下載報告 | https://<host_name>/api/projects/<project_id>/versions/<version_id>/reports/<report_id>/download | | | Visit components with filter | 針對指定的 project 的 components 加上條件式篩選 | https://<host_name>/api/projects/<project_id>/versions/<version_id>/components |

Fortify On Demand(FoD)

Fortify On Demand(以下簡稱 FoD),擁有公開的官方 Swagger 文件,提供你各式各樣的 APIs 使用,他還有一個很便利的功能就是可以直接用此 Swagger 文件模擬並發送請求至 APIs,所以當 Call 各種 APIs 都回傳 Error 時,可以考慮直接用這個模擬器先試試看。

今天介紹以下 API Endpoints:

  • Oauth Authentication: https://api.ams.fortify.com/oauth/token
  • Get Application: https://api.ams.fortify.com/api/v3/application
  • Get Release (with filter): https://api.ams.fortify.com/api/v3/release or https://api.ams.fortify.com/api/v3/application/<application_id>/release

Oauth Authentication

在 Call 任何 FoD 的 APIs 前,你必須請求這個 Oauth API Endpoint,取得回傳的 token,並使用那個 token 請求其他 FoD 的 API。

// POST https://api.ams.fortify.com/oauth/token
// Header
{
  "scope": "api-tenant", // scope 要去你的帳號設定查看。
  "grant_type": "password", // 取得權限是用password形式。
  "username": "<tenant>\\<user_name>", // tenant跟user name也是要去帳號設定查看,記得注意一個反斜線 \,但若是在程式碼內需要兩個反斜線\\,防止程式辨別特數字元。
  "password": "<personal_token>" // 注意此項不是放你的密碼,是放你在設定裡產生的那組token。
}

response

{
  "access_token": "ZGHJ3274JDK" //送出請求後,可從response獲得一組token,這邊只是隨便寫代表,實際上很長。
}

Get Application

拿到專門 call API 的 token 後,我們就能使用那組 token 請求其他的 API 功能,首先我們先取得 Applications,這組 APIs 會取得列表 Application。

// GET https://api.ams.fortify.com/api/v3/application
// Header
{
  "Content-Type": "application/json",
  "Accept": "application/json",
  "Authorization": "Bearer <token>" // 這邊要放你剛剛call Oauth取得的token
}

response

{
    ...,
    "items": [{
        "applicationName": "",
        "applicationId": "",
        ...
    }, {
        ...
    }
    ]
}

透過此 APIs,你會取得多個 Application,你可以知道他們的 Name 及 applicationId,方便查詢其他 API 使用,此 API 支援 filter,可以直接查找你想要找的 Application,我會在下個 API 介紹,filter 用法都差不多。

Get Release (with filter)

你可以選擇一次獲取全部的 release 或者獲取指定 application 下的 release,後者需要 applicationId。

// GET https://api.ams.fortify.com/api/v3/release or https://api.ams.fortify.com/api/v3/application/<application_id>/release
// Header
{
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Authorization": "Bearer <token>", // 這邊要放你剛剛call Oauth取得的token
}

// Params -> 注意是Parameters,不是Body,這是Get請求,Python請放requests.get(params=<params>),postman請把參數放在Params,我被這坑到。
{
    "filter": "ReleaseName:<service_name>" // 這是單一filter案例,多個請用 + 分開
}

這邊要特別注意 filter 放個內容是 <你要filter的欄位名稱1>:<你要filter的值1>+<你要filter的欄位名稱2>:<你要filter的值2>,所以假設我今天想要 filter ReleaseNameppt-scanner,就放ReleaseName:ppt-scanner,然後請夾帶至請求的 Parameters 欄位,這是 Get 方法。

至於能 filter 什麼,文件上好像沒寫,但可以仔細觀察 release API 回傳的 Response 會列出所有 Release 的服務,會順帶回傳該 Release 的所有屬性(資訊),你可以去推哪些屬性是可以被拿來當作 Filter 的欄位。

Black Duck (BD)

Black Duck (以下簡稱 BD),其 APIs 文件為非公開,你可以去網頁右上方的?REST API Developer Guide,BD 的 API 文件,我個人覺得寫得很詳細,不過還是會有些需要注意的地方,但已經很完整了。

今天介紹以下 BD API Endpoints,由於 Black Duck APIs 是公司自架,自己的網域,所以以下只會列出後面的 API 連結,實際請求時,你要將 host_name 換成你們的網域:

  • Authentication: https://<host_name>/api/tokens/authenticate
  • Query project: https://<host_name>/api/projects
  • Get version: https://<host_name>/api/projects/<project_id>/versions
  • Generate Version Details Report: https://<host_name>/api/versions/<version_id>/reports
  • Download Version Details Report: https://<host_name>/api/projects/<project_id>/versions/<version_id>/reports/<report_id>/download
  • Visit components with filter: https://<host_name>/api/projects/<project_id>/versions/<version_id>/components

Authentication

跟上方的 FoD 一樣,在 Call 任何其他 BD 的 APIs 前,你必須請求這個 authenticate API Endpoint,取得回傳的 token,並使用那個 token 請求其他 BD 的 API。

// POST https://<host_name>/api/tokens/authenticate
// Header
{
  "Accept": "application/vnd.blackducksoftware.user-4+json",
  "Authorization": "token <personal_token>" // 你可以在Black Duck的網站右上方人頭 -> Access token 產生一個 personal token,然後注意如果你需要產報告,你要把write權限打勾。
  // Authorization的值,請拜託一定要在前面加 token,才把personal token寫下去,我當時卡這個卡的一臉懵,卡一陣子,例子: "Authorization": "token GH2395JKls=="
}

response

{
  "bearerToken": "ZGHJ3274JDK" //送出請求後,可從response獲得一組token,這邊只是隨便寫代表,實際上很長。
}

Query project

拿到專門 call API 的 token 後,我們就能使用那組 token 請求其他的 API 功能,這個 API 可以取得所有的 projects,但你可以請 API 找出你想找的 project filter。

// GET https://<host_name>/api/projects
// Header
{
    "Authorization": "Bearer <bearerToken>", // 這邊要放你剛剛取得的BearerToken
}
// Params -> 沒錯又是Parameters,這是get請求,不要放去body。
// 如果你需要 limit限制回傳的筆數或者filter過濾你要找的projects,才需要這個parameters,如果沒有params,會回傳全部的projects (有限制筆數)
{
    "limit": 5, // 我限制5筆就好
    "q": "name:<filter_name>" // 沒錯就是一個 q,這欄是filter,如果你想查指定名稱的project,就照這樣填,不確定其他可以filter的欄位。
}

response

{
    ...,
    "items": [{
        "name": "",
        "_meta": {
            "href": ""
        },
        ...
    }, {
        ...
    }
    ]
}

我發現就是呢,回傳的 project 資訊好像沒有 project_id,所以我當初是在project_metahref連結這個欄位,然後split("/")[-1] 取最後一段 id,那是該 project 的 id。

Get version

當你在上一步驟取得 project 後,可以使用這個 api 取得所有隸屬於那個 project 的版本 versions。

// GET https://<host_name>/api/projects/<project_id>/versions
// Header
{
  "Authorization": "Bearer <bearerToken>" // 這邊要放你剛剛取得的BearerToken
}

記得在網址放上指定的 project_id

response

{
    "items": [{
        "versionName": "master",
        ...,
        "_meta": {
            "href": ""
        },
    },
    {
        "versionName": "version_1",
        ...,
        "_meta": {
            "href": ""
        },
    }
    ]
}

沒錯,就是呢,他又不會回傳 version_id 了,我們又得去_meta裡的href切割網址並找出他的 version_id。

split("/")[-1],一樣取最後一個,回傳的網址應該會長得像 https://<host_name>/api/projects/<project_id>/versions/<version_id>

Generate Version Details Report

現在我們有 project_idversion_id 了,那我們可以產出報告了。我只有研究怎麼產生 Version Details 報告,其他三種不太理解,但 API 寫得很清楚。

// POST https://<host_name>/api/versions/<version_id>/reports
// 沒錯很奇怪,居然不需要project_id,我懷疑他萬一有重複的version_id會怎麼判斷。
// Header
{
    "Content-Type": "application/json",
    "Authorization": "Bearer <bearerToken>", // 這邊要放你剛剛取得的BearerToken
}

// Body -> 終於是 body了,post請求哈。
{
    "reportFormat": "CSV", // 這邊我們選csv,但我記得可以填JSON
    "reportType": "VERSION",
    "caregories": ["VERSION", "COMPONENTS", "SECURITY", "PROJECT_VERSION_CUSTOM_FIELDS"] // 這邊就是你產報告時,他會有Categories 的打勾勾選項,你選哪些,這邊就填哪些。
}

如果 categories 的部分你不是很確定的話,你可以去網站實際產一份報告,打勾你要的欄位,然後打開開發者工具-> Network,點選產生報告,Network 會紀錄他 call 了哪個 API,然後你可以去看他的 Body 送了什麼,複製過來就行。

response

{
    "links": {
        "download": {
            "url"
        }
    }
}

Reponse 的格式我不太確定,因為我是在 python 寫,回傳的好像是實際一個物件,你可以從links -> download -> url 去取得 report_id

又是老招 split("/")[-1],對 url 取最後一段,那就是report_id

我在想搞不好其實都能獲得 project_id, version_id, report_id,只是我文件沒看清楚。

Download Version Details Report

產生報告後,記住他不是馬上完成,所以如果你是撰寫程式自動化,記得要寫 while 迴圈每 7-10 秒 call 一次 (沒意外大概 30 秒內都能完成)。

如果報告沒完成會回傳 HTTP Status 412,但注意的是不只報告沒產好會回傳 412,其他錯誤也會回傳 412,要注意看回傳 412 額外的 message 是否指報告還沒產好。

上一步驟產生的報告,這步驟我們就下載下來。

// GET https://<host_name>/api/projects/<project_id>/versions/<version_id>/reports/<report_id>/download
// 真奇妙,產報告不需要 project_id,載報告需要,難道report_id是會重複的?
// Header
{
  "Authorization": "Bearer <bearerToken>" // 這邊要放你剛剛取得的BearerToken
}

一條 get 就把內容載下來了,response 的 content 是 bytes 檔,我不知道怎麼用 json 呈現,以下是展示怎麼把回傳的 bytes 文字存擋。 (用 python)

# 如果是下載在local
import io
import zipfile

response = requests.get(url, headers=headers)
if response.status_code == 200:
    z = zipfile.ZipFile(io.BytesIO(response.content))
    z.extractall("./")

# 如果是想存進 AWS S3
import io
import boto3
import zipfile

s3_client = boto3.client("s3")

s3_bucket = "<bucket_name>"

response = requests.get(url, headers=headers)
if response.status_code == 200:
    z = zipfile.ZipFile(io.BytesIO(response.content))
    for file_info in z.infolist():
        file_name = file_info.filename

        # 讀取 file content
        file_content = z.read(file_name)

        save_key = f"<prefix>/{file_name}"

        # 上傳檔案內容到S3
        s3_client.upload_fileobj(io.BytesIO(file_content), s3_bucket, save_key)

Visit components with filter

最後一個要介紹的是,訪問指定的project_id與指定版本version_id的 components,並添加 filter,去過濾 components。

此 API,我只有嘗試加一個 filter,文件裡有寫可以加多個 filters。

// GET https://<host_name>/api/projects/<project_id>/versions/<version_id>/components
// 一樣要填 project_id 和 version_id

// Header
{
    "Authorization": "Bearer <bearerToken>", // 這邊要放你剛剛取得的BearerToken
}

// Params -> Parameters 哈,這邊用GET方式
{
    "limit": 10, // 看你要不要設筆數
    "filter": "bomPolicy:in_violation" // filter -> 欄位:選項
}

這邊抓出有 violation 的 components。

response

// 如果沒有找到filter符合的資料,items會留空 []。
{
  "items": [
    {
      "componentName": "",
      "componentVersionName": ""
    },
    {
      "componentName": "",
      "componentVersionName": ""
    }
  ]
}

如此一來就能透過添加 Params 的方式,去 filter components。

那如果你不確定 filter 內容怎麼寫,一樣可以直接去網站,進到 components,打開開發者工具 -> Network,然後添加 filters,你就會看到Network抓到的 Params 資訊。

或者可以透過網址直接辨識: https://<host_name>/api/projects/<project_id>/versions/<version_id>/components?filter=bomPolicy:in_violation,在?filter=後面的值辨識那些欄位跟植,然後寫進請求裡的 Params 裡嘗試。

好啦,以上就是此次 Fortify On Demand 和 Black Duck APIs 的研究結果,希望有幫到你。