公司有一項冗長繁瑣的程式資安審核流程使用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
orhttps://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 ReleaseName
是ppt-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
的 _meta
抓href
連結這個欄位,然後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_id
跟 version_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的研究結果,希望有幫到你。