如何產(chǎn)出規(guī)范、安全、高質(zhì)量的代碼?
對(duì)于一個(gè)軟件開發(fā)團(tuán)隊(duì),可以通過哪些代碼質(zhì)量指標(biāo)和掃描方法讓團(tuán)隊(duì)產(chǎn)出規(guī)范、安全、高質(zhì)量的代碼?讓開發(fā)團(tuán)隊(duì)運(yùn)行的安全、透明、可靠?本文總結(jié)了其中一些實(shí)踐和工具,包含常見代碼質(zhì)量掃描工具、代碼質(zhì)量指標(biāo)、第三方依賴管理、安全運(yùn)維等幾個(gè)方面,主要適用于 Java/JavaScript 技術(shù)棧的 web 項(xiàng)目,希望對(duì)于想要規(guī)范化自己的項(xiàng)目的 Tech Lead 有所幫助。
代碼掃描和常見質(zhì)量指標(biāo)
“禍患常積于忽微”,往往一些奇怪的 bug 都是一些不規(guī)范的小問題造成的。德國飛機(jī)渦輪機(jī)的發(fā)明者帕布斯·海恩提出的一個(gè)在航空界關(guān)于飛行安全的法則,法則指出: 每一起嚴(yán)重事故的背后,必然有 29 次輕微事故和 300 起未遂先兆以及 1000 起事故隱患。應(yīng)用于軟件開發(fā)中,如果項(xiàng)目中代碼混亂不堪,必然會(huì)在某個(gè)時(shí)候最終爆發(fā)大量的問題。
這里整理了一些常見的掃描工具和代碼質(zhì)量指標(biāo),可以在搭建項(xiàng)目基礎(chǔ)設(shè)施時(shí)引入,用于自動(dòng)化的檢查代碼中潛在的問題,達(dá)到控制代碼產(chǎn)出質(zhì)量的目的。
掃描工具
(1) checkstyle:checkstyle 是常用于 java 項(xiàng)目的掃描工具,檢查源代碼是否與代碼規(guī)范相符,檢查項(xiàng)目主要包括:Javadoc 注釋、imports、過長的類和方法、空格、重復(fù)文件、圈復(fù)雜度等,默認(rèn)使用 sun 的代碼規(guī)則,也可以配置自定義的代碼規(guī)則,例如阿里就發(fā)布了相應(yīng)的檢查規(guī)則。
(2) findbugs:通過 Bug Patterns 的概念,尋找代碼中可能出現(xiàn)的 bug,檢查項(xiàng)目主要包括:不良編程習(xí)慣導(dǎo)致的問題、性能問題、安全問題、線程問題等。例如,應(yīng)使用 equals 判斷相等,而不是 “ =” 操作符、流需要關(guān)閉、線程資源需要釋放等問題。findbugs 的模式庫對(duì)編程經(jīng)驗(yàn)也有較好的提升作用。還可以導(dǎo)入和編寫自己的 Bug Patterns 完善檢查機(jī)制。
(3) simian:simian 是一個(gè)用于檢查重復(fù)和相似代碼的工具,它的重復(fù)檢查類似于論文查重,會(huì)提示一定的相似度??梢詥为?dú)運(yùn)行,也可以作為 checkstyle 插件來使用,相對(duì)來來說比較小眾。
(4) pmd:pmd 是一款跨語言的通用靜態(tài)掃描工具,具備一部分 checkstyle、findbugs 的功能,不再贅述。
(5) ESlint/TSlint:前端界的 checkstyle , TSlint 設(shè)計(jì)用來做 TypeScript 類型檢查,ESlint 作為代碼風(fēng)格檢查工具。不過現(xiàn)在 ESlint 也提供了TypeScript 類型檢查功能,基本上 ESlint 能整合這兩個(gè)功能。由于性能問題, TypeScript 也采用了 ESLint 作為 TSlint替代的檢查工具。
(6) SonarQube:SonarQube 是一款用于代碼質(zhì)量管理的開源工具,它主要用于管理源代碼的質(zhì)量。SonarQube 和上面的工具不太一樣,SonarQube 設(shè)計(jì)目的是提供一個(gè)平臺(tái),通過插件的方式提供對(duì)各個(gè)語言進(jìn)行支持,也可以和 checkstyle、pmd、simian 等工具進(jìn)行集成。SonarQube 一般需要單獨(dú)部署成一個(gè)服務(wù),提供數(shù)據(jù)庫,可以記錄掃描結(jié)果等信息。
(7) npm audit:npm audit 是 npm 6 之后的版本 自帶的一個(gè)前端安全掃描工具,可以掃描 npm 依賴中的潛在的漏洞威脅。這些引入的漏洞可能威脅用戶開發(fā)的機(jī),另外也可能被帶入 bundle 文件發(fā)布到線上,帶來安全問題。目前 npm audit 會(huì)在 npm install 完成后自動(dòng)執(zhí)行,需要留意安全威脅報(bào)告。
(8) Fortify SCA:Fortify SCA(Source Code Analyzer) 是一款非常優(yōu)秀的代碼安全掃描工具,用于分析代碼中潛在的安全問題。通過調(diào)用語言的編譯器或者解釋器把代碼(Java、C、C++等源代碼)轉(zhuǎn)換成一種中間媒體文件 NST(Normal Syntax Trcc),然后通過模式匹配相關(guān)的方式抓取存在于漏洞庫中的漏洞。例如,上傳的文件沒有做檢查等 XSS 攻擊。
(9) OWASP Dependency-Track:開放式 Web 應(yīng)用程序安全項(xiàng)目(OWASP)是一個(gè)非營利組織,提供了很多安全標(biāo)準(zhǔn)、數(shù)據(jù)庫、社區(qū)和培訓(xùn)。其中一個(gè)工具就是 OWASP Dependency-Track,可以對(duì)第三方依賴包中的知名漏洞進(jìn)行檢查,掃描結(jié)果受到漏洞數(shù)據(jù)庫的更新影響。
(10) archunit 架構(gòu)規(guī)范檢查:前面的檢查是代碼層面,archunit 可以用于代碼架構(gòu)檢查,可以定義規(guī)則檢查每個(gè)包中的實(shí)現(xiàn)是否符合規(guī)范。例如,controller 包中的類不能實(shí)現(xiàn) service 的接口,repository 下的類必須實(shí)現(xiàn) Repository 接口。通過 archunit 可以減少 codereview 的工作量,避免項(xiàng)目的結(jié)構(gòu)被破壞。
統(tǒng)計(jì)工具
sloccount、sourcemointor 這兩個(gè)工具可以用于統(tǒng)計(jì)代碼數(shù)量,包括行數(shù)、文件數(shù)、注釋等。除了在項(xiàng)目中掃描 bug 之外,配置代碼統(tǒng)計(jì)工具可以對(duì)項(xiàng)目有一個(gè)整體的認(rèn)知。其他的掃描工具還很多,例如 coverity、codemars、binscope、synk、appscan、retire.js 等工具,不再一一列舉。
最佳搭配
這幾款工具之間的功能有所重疊,在實(shí)際工作中,我們可以根據(jù)上面推薦的關(guān)注的點(diǎn),重點(diǎn)清除這些問題。這些掃描工具全部用上除了會(huì)帶來團(tuán)隊(duì)壓力和維護(hù)成本之外,代碼質(zhì)量不會(huì)隨著引入的插件增多。除開有質(zhì)量團(tuán)隊(duì)的大廠提供這些掃描平臺(tái)外,敏捷團(tuán)隊(duì)往往不會(huì)太大,團(tuán)隊(duì)持續(xù)關(guān)注一個(gè)精簡的掃描組合更好。
Java 后端:
- checkstyle Java 代碼風(fēng)格守護(hù),Java 項(xiàng)目至少應(yīng)該配置一個(gè)默認(rèn)的 checkstyle 規(guī)則。至少讓項(xiàng)目干凈,沒有無用、重復(fù)的代碼,以及超大的類和方法。建議做到每次提交代碼前檢查。
- findbugs 常見不規(guī)范的代碼檢查,一些空指針、equals 檢查非常有用,而且 IDE 的插件也很好用。
前端:
- eslint 守護(hù) JavaScript 代碼風(fēng)格,eslint 搭配一個(gè) .editorconfig ,可以方便的讓編輯器保持同 eslint 一致的代碼風(fēng)格。
- npm audit 項(xiàng)目中第三方包的威脅掃描,npm 自帶無需額外安裝,npm 6 以后自運(yùn)行,需要關(guān)注并修復(fù)報(bào)出的安全問題。
安全:
- fortify 掃描代碼中的漏洞,用它檢查出來的大部分安全問題都是注入攻擊、XSS 等攻擊,這些問題明顯可以在開發(fā)過程中避免??梢宰鳛?Jenkins 插件配置,和單元測試作為同一階段運(yùn)行。
- OWASP 插件 用來掃描第三方依賴漏洞,因?yàn)轫?xiàng)目中的依賴不會(huì)像源代碼一樣頻繁變化,推薦使用 Jekins 插件,定期執(zhí)行即可。
為什么不用 SonarQube 呢,SonarQube 是一個(gè)非常優(yōu)秀的代碼質(zhì)量開放平臺(tái),需要單獨(dú)的配置安裝,需要花費(fèi)額外的時(shí)間維護(hù),對(duì)于小團(tuán)隊(duì)來說成本較高,如果有專門的質(zhì)量團(tuán)隊(duì)可以考慮維護(hù)一套。
常用代碼質(zhì)量指標(biāo)參考
- 編譯告警數(shù),大部分程序員基本上忽略 warning,但是編譯器出現(xiàn)了告警是一種不好的體現(xiàn),意味著軟件可能工作,但是存在不好的實(shí)踐,而這種不確定性,會(huì)帶來不確定的 bug 最終讓人一頭霧水。編譯過程中的告警,盡量消除掉,編譯告警的值推薦消除到 0。
- 平均函數(shù)代碼行數(shù),過大的函數(shù)會(huì)導(dǎo)致閱讀困難,而且往往過大的函數(shù)職責(zé)不夠單一,一般將一個(gè)方法代碼行數(shù)控制到 30 - 50 行。
- 平均文件代碼行,和平均函數(shù)代碼行一樣,過長的文件一樣難以維護(hù),一般一個(gè)文件10多個(gè)方法,因此文件的代碼行數(shù)一般控制到 300 - 500 行。
- 冗余代碼,有時(shí)候我們代碼中可能存在未使用的方法、變量等代碼,這讓維護(hù)者一頭霧水,通常需要清零。
- 總文件重復(fù)率,出現(xiàn)重復(fù)文件的次數(shù)。除了編寫單元測試的情況下,業(yè)務(wù)代碼不應(yīng)該出現(xiàn)重復(fù)代碼,推薦值為 0。
- 總代碼重復(fù)度,代碼的重復(fù)度檢查,限于掃描工具的識(shí)別模式,需要有一定的容忍度,推薦值在 5% - 10%
- 平均函數(shù)圈復(fù)雜度,圈復(fù)雜度用來衡量一個(gè)模塊判定結(jié)構(gòu)的復(fù)雜程度。如果一個(gè)方法內(nèi)部有大量的 if 語句嵌套,意味著這個(gè)方法的實(shí)現(xiàn)質(zhì)量低下,且程序復(fù)雜度高不利于維護(hù),推薦值小于 5%。
- 安全告警,如果配置了安全掃描工具,例如 Fortify,安全威脅應(yīng)該被清零。
- 代碼缺陷,如果配置了缺陷掃描工具,例如 Findbus,需要清零。
第三方依賴規(guī)范化
軟件開發(fā)過程中,不可避免的需要引入第三方或者開源軟件包作為庫或者框架引入。“第三方” 其實(shí)不是一個(gè)軟件工程術(shù)語,現(xiàn)今在軟件行業(yè)里面的理解是:第一方為自研的軟件,第二方為內(nèi)部發(fā)布的軟件,第三方為從社區(qū)或者外部商業(yè)途徑引入的軟件包。對(duì)于個(gè)人開發(fā)者而言,面向“搜索引擎”編程往往將來源不明的代碼片段和程序包引入到項(xiàng)目中。對(duì)于企業(yè)來說,考慮到的不僅僅是功能是否能實(shí)現(xiàn),還要考慮引入時(shí)帶來的成本和問題,例如是否需要授權(quán)、開源協(xié)議是否合理、是否會(huì)帶來安全威脅。企業(yè)對(duì)于第三方依賴的引入分為幾種情況:
- 作為開發(fā)工具引入,例如 gcc、Jenkins,基本沒有開源協(xié)議問題,但是需要注意開發(fā)機(jī)、CI 會(huì)有安全風(fēng)險(xiǎn)。Jenkins 曾出現(xiàn)過漏洞,CI 服務(wù)器被當(dāng)做遠(yuǎn)程礦機(jī)使用。
- 作為服務(wù)部署使用(SaaS),部分開源協(xié)議會(huì)限制這種使用方式,第三方依賴的安全問題會(huì)威脅服務(wù)器。
- 通過軟件包再發(fā)布,大部分開源軟件對(duì)這種使用方式有較多要求,例如 GPL 開源協(xié)議具有傳染性,要求使用了 GPL 的項(xiàng)目也要開源。
- 拷貝源代碼引入項(xiàng)目,非常不推薦這種方式,盡量通過包管理的方式引入。
引入第三方依賴需要充分考慮,盡可能最小成本的引入。在一個(gè) React 的前端項(xiàng)目中,有不熟悉的工程師,為了使用一個(gè)簡單的手風(fēng)琴效果,引入了整套 bootstrap。不僅破壞了使用 React 的最佳實(shí)踐,而且讓輸出的 bundle 文件大小激增數(shù)倍,造成首屏加載的性能問題。
常見商業(yè)友好的開源協(xié)議
商業(yè)用戶常用的開源協(xié)議實(shí)際上只有6種左右,即 LGPL、Mozilla、GPL、BSD、MIT、Apache,另外還有極其寬松的 The Unlicense,但采用的開源軟件不多。GitHub 提供了一個(gè) license 清單的列表 https://choosealicense.com/licenses/,我根據(jù)開源協(xié)議的寬松程度,整理了一個(gè)列表,方便查看:
幾乎所有的開源協(xié)議有一個(gè)共同的注意事項(xiàng):采用該開源協(xié)議的軟件項(xiàng)目,不提供任何責(zé)任轉(zhuǎn)移和質(zhì)量保證。也就是說采用開源軟件造成的法律問題和開源項(xiàng)目無關(guān),另外需要使用者承擔(dān)因質(zhì)量問題造成的所有后果。另外,除了引入的程序包之外,字體、圖片、特效音、手冊(cè)等媒體資源也算廣義上的“軟件”需要考慮開源協(xié)議和使用場景。
第三方依賴管理
對(duì)項(xiàng)目中出現(xiàn)的任何第三方依賴有效的管理有非常重要的意義,通過掃描工具,識(shí)別出項(xiàng)目中是否有源碼、jar包、二進(jìn)制文件是否來源于某個(gè)開源項(xiàng)目。任何的第三方軟件需要申請(qǐng)入庫管理(內(nèi)部其他團(tuán)隊(duì)申請(qǐng)通過可以直接使用),質(zhì)量團(tuán)隊(duì)對(duì)申請(qǐng)的軟件進(jìn)行評(píng)估:
- 是否有開源義務(wù)需要履行
- 引入的第三方依賴是否有 CVEs等漏洞
- 第三方開源軟件是否仍然在維護(hù)
質(zhì)量團(tuán)隊(duì)根據(jù)上面的一些條件,決定出申請(qǐng)的軟件能否在項(xiàng)目中使用,允許被采用的軟件會(huì)定義出優(yōu)選級(jí)別,優(yōu)先推薦團(tuán)隊(duì)使用較為優(yōu)選的軟件,并對(duì)項(xiàng)目整體的優(yōu)選率有一定要求。如果項(xiàng)目中出現(xiàn)了無法識(shí)別的二進(jìn)制文件、非約定目錄下的代碼片段,需要報(bào)備。通過良好的依賴管理和規(guī)范化,能減少不良第三方依賴的引入,讓軟件項(xiàng)目透明、可信。一些商業(yè)公司提供這些完整的服務(wù),例如 fossid、blackduck、code-climate 等。
運(yùn)維安全
大的軟件公司,往往有一堆流程和要求。雖然一線開發(fā)對(duì)堡壘機(jī)、防火墻、各種安全規(guī)范顯得不耐煩,但這些安全措施也在保護(hù)開發(fā)者。
1. 防火墻用于環(huán)境隔離
往往開發(fā)者理解的防火墻用于防止網(wǎng)絡(luò)入侵、審計(jì)、入侵檢測等功能,除此之外,防火墻還可以用于各個(gè)環(huán)境的隔離。一般來說,企業(yè)對(duì)于生產(chǎn)環(huán)境的數(shù)據(jù)控制比較嚴(yán)格,不會(huì)將生產(chǎn)環(huán)境的權(quán)限交給團(tuán)隊(duì)所有開發(fā)者,但網(wǎng)絡(luò)連接有可能疏漏。曾經(jīng)出現(xiàn)過一次線上事故,由于配置文件錯(cuò)誤,將原本應(yīng)該連接到測試的數(shù)據(jù)庫連接到了生產(chǎn)環(huán)境,造成大量臟數(shù)據(jù)寫入。如果通過防火墻規(guī)則對(duì)各個(gè)環(huán)境進(jìn)行隔離,這類問題將不會(huì)出現(xiàn)。另外也可以設(shè)計(jì) DMZ 區(qū),將面向用戶側(cè)的網(wǎng)關(guān)部署到 DMZ 區(qū),僅僅開放必要的端口給網(wǎng)關(guān),實(shí)現(xiàn)內(nèi)外網(wǎng)的物理隔離。同時(shí),對(duì)整個(gè)系統(tǒng)的防火墻策略應(yīng)該清晰地記錄,否則在做大的基礎(chǔ)設(shè)施更新時(shí),梳理出所有的防火墻策略,是一件比較困難的事情。
2. 憑據(jù)管理
項(xiàng)目中會(huì)用到大量的憑據(jù),例如數(shù)據(jù)庫、第三方系統(tǒng)對(duì)接的 key,使用明文不是一件好事。理想的情況下,對(duì)項(xiàng)目中所有的密碼信息進(jìn)行掩蓋(mask),避免 CI、日志中敏感信息的泄露。有很多種方法可以掩蓋項(xiàng)目中的密碼信息:
- 使用環(huán)境變量對(duì)密碼信息進(jìn)行覆蓋。
- 使用Spring boot 的項(xiàng)目可以配置 jasypt,使用 jasypt 將密碼加密,將生成的加密串配置 ENC(加密串) 到工程的配置文件中。加密過程可以加鹽作為解密的憑據(jù),“鹽” 可以不存放到工程中,在工程部署的時(shí)候注入即可。
- 如果使用 Jenkins 等 CI/CD 工具,可以使用構(gòu)建平臺(tái)提供的憑證管理工具。
- 如果使用 Spring cloud,可以使用 spring cloud vault 組件部署一個(gè)憑證管理服務(wù)
另外,建議不要用任何個(gè)人憑據(jù)用作系統(tǒng)對(duì)接,應(yīng)該使用一個(gè)公共的應(yīng)用憑據(jù)。
3. 堡壘機(jī)
一般來說我們管理服務(wù)器,所有的運(yùn)維操作需要通過堡壘機(jī)進(jìn)行操作。開放 22 等高危端口,允許開發(fā)者直接登錄到服務(wù)器是一種不安全的做法。堡壘機(jī),通俗的來說是跳板機(jī) + 監(jiān)控。最初使用的跳板機(jī)配置了兩張網(wǎng)卡,用于連接開發(fā)環(huán)境和生產(chǎn)環(huán)境,并沒有監(jiān)控功能。在此基礎(chǔ)上,堡壘機(jī)增加了統(tǒng)一運(yùn)維管理的功能,往往需要兩步驗(yàn)證(SMS 或 Email),并對(duì)所有的操作進(jìn)行記錄和監(jiān)控。在需要團(tuán)隊(duì)參與運(yùn)維工作的場景中,非常有必要部署一套堡壘機(jī)服務(wù),并使用 LDAP 對(duì)接到團(tuán)隊(duì)成員的 ID 上,便于集中運(yùn)維管理。
4. 定期對(duì)系統(tǒng)軟件掃描
Linux 系統(tǒng)往往有云廠商推送安全補(bǔ)丁和風(fēng)險(xiǎn)提示,但是安裝到服務(wù)器上的軟件,例如 JDK、nodejs,需要自己檢查安全問題。因此需要在系統(tǒng)中安裝并定期運(yùn)行 CVEs 檢查并及時(shí)更新。有一款 cvechecker 可以幫助運(yùn)維人員,編寫一個(gè)腳本定期運(yùn)行 cvechecker 檢查系統(tǒng)中已知的軟件是否存在 CVEs 漏洞,并提醒開發(fā)者及時(shí)更新。
寫在后面
剛開始工作時(shí)候,喜歡動(dòng)態(tài)的、靈活的編程語言,討厭的死板的、套路化的編程語言,然而需要很長一段時(shí)間,才能意識(shí)到 “約束是程序員的朋友”。對(duì)一些安全知識(shí)了解的來源大多來自修復(fù) SonarQube 的經(jīng)歷,使用 findbugs 也讓我對(duì) Java 基礎(chǔ)認(rèn)識(shí)的更加深刻。
類似的,在使用一些框架、平臺(tái)的時(shí)候往往存在大量的限制,有時(shí)候開發(fā)者難以意識(shí)到 “限制” 正是框架、平臺(tái)的作者 “保護(hù)” 應(yīng)用開發(fā)者的一種方式。有一些開發(fā)者以 Hack 框架、平臺(tái)為樂,但是這樣會(huì)帶來潛在的隱患,在用戶量上來之后負(fù)面效應(yīng)表現(xiàn)的尤為明顯。
項(xiàng)目的規(guī)范化對(duì)于 Tech Lead來說可以減少程序的運(yùn)行事故和 codereview 時(shí)間,對(duì)于團(tuán)隊(duì)來說也許可以少加班吧。
【本文是51CTO專欄作者“ThoughtWorks”的原創(chuàng)稿件,微信公眾號(hào):思特沃克,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】