对开源项目的关键性评分

7erry

在现代世界中,复杂的系统和系统系统是社会和企业运作不可或缺的一部分,能够理解和管理这些系统和组件可能给它们支持的使命带来的风险变得越来越重要。然而,在资源有限的世界里,不可能对所有资产都应用平等的保护。

关键性分析可帮助行业根据资产对运营和安全的重要性评估并识别关键资产,进而确定维护活动的优先级,这种方法有助于识别对维持运营和降低安全风险至关重要的关键资产。通过将维护工作集中在这些关键组件上,组织可以防止停机并最大程度地减少潜在危险。它提供了一个决策框架,使行业能够有效地分配资源。通过优先考虑非常重要的领域,企业可以在降低风险和提高效率之间取得平衡。临界性分析在将理论风险转化为实际信息方面发挥着至关重要的作用,使行业能够就将维护工作投入到何处做出明智的选择。

Criticality Score Introduction

大大小小的开源项目都面临着资源分配的问题,包括所需的时间、资源和关注度。需要一个将关键项目与可为其提供支持的组织紧密联系起来的方法

2020 年,谷歌联合开源安全基金会 (OpenSSF) 推出 “Criticality Score”,这是一个能够通过具体指标来量化开源项目的评估工具。这些指标包括开源项目创立时间长度、贡献者数量、提交频率、过去一年的发版数量、过去 90 天内关闭和更新的 issue 数量、回复频率、commit 信息中提到的项目数量以及其他参数。
基于上述指标,只需提供项目的 GitHub 仓库 URL 地址,Criticality Score 就会计算出区间为 0-1 的分数来表示开源项目在此标准下的关键性。

OpenSSF Criticality Score 的目的在于

  • 为每个开源项目生成 “关键性” 分数
  • 创建开源社区所依赖的重要项目列表
  • 使用这些数据来主动改善这些重要项目的安全态势

2023 年 2 月,Criticality Score 发布了重要更新 v2.0,官方称这是一次 “大改造”。此版本采用 Go 语言进行完全重写,并且不再依赖 Python。此外还对许多评分指标进行了完善,以及修复错误和增强功能。

Criticality Score Algorithm

Criticality Score 使用“包”这一术语称呼被评分的单位。一个包的评分需要综合考虑该包在多个指标下的表现,而 Criticality Score 的评分算法采用了以下默认指标:

参数 (Si)权重 (αi)最大阈值 (Ti)描述采用原因
created_since1120项目创建的时间(以月为单位)较旧的项目有更高的机会被广泛使用或依赖
updated_since-1120自项目上次更新以来的时间(以月为单位)最近没有提交的并且也不被维护项目有较低的被依赖度
contributor_count25000项目贡献者的数量(有提交)不同的贡献者参与表明了项目的关键性
org_count110贡献者所属的不同组织的数量表示跨组织依赖性
commit_frequency11000去年平均每周提交次数较高的代码变更在某种意义上表明了项目的关键性。当然,其对漏洞也更敏感
recent_releases_count0.526去年发布的数量频繁发布表明用户依赖度较高。但是这个参数权重较轻,因为该参数并不总是使用
closed_issues_count0.55000过去 90 天内关闭的问题数量表示贡献者高度参与,并专注于解决用户的issue。较低的权重,因为它依赖于项目贡献者
updated_issues_count0.55000过去 90 天内更新的问题数量表示贡献者参与度高。较低的权重,因为它依赖于项目贡献者
comment_frequency115过去 90 天内每个问题的平均评论数表示用户活跃度和依赖性高
dependents_count2500000N在commit 消息中提及的项目数量表示该仓库的使用情况,通常用在在版本迭代中。此参数适用于所有语言,包括没有包依赖关系图的 C/C++(虽然是 hack-ish)。计划在不久的将来添加包依赖树

这些值可以通过可执行程序的 -scoring-config 参数或 original_pike.yml 配置文件进行修改,并正根据相关社区的探讨调优

Criticality Score 的评分指标可以由用户自行定义与增减,而所有指标都有着其对应的 Signal ,一个非负值用以量化该指标下项目的关键性。每一个包都有多个指标,但显然不同指标在同一评价体系下与同一指标在不同评价体系下的重要程度不可能完全相同,因此每个 Signal 除了具有数值 Si , 还具有可任意设置的正权重 αi

出于统计学的考虑(许多 Signal 具有类似 Zipf 的分布),在我们量化一个包在特定指标下的关键性时,与权重相乘的不应该是 Si , 而是 log(1+Si) 以通过非线性的方式缩放 Signal (加 1 避免负值),毕竟一个有着 10,000 个依赖的包肯定比一个只有 1,000 个依赖的包重要,但不会重要十倍。

人们在评分采用的评分体系大概有两种,一种是不设阈值的,一种是设立阈值的。例如当我为一部动画基于画面,剧情,摄影这三个指标以三分为满分评了 2.5 分时,在第一种评分体系下,这部动画可能出现一些极端情况,例如它的画面是 0.5 分,摄影是 0 分,但剧情实在太出彩了,我为它的剧情打了 2 分的高分。事实上我也确实是这样打分的。但是在不少评分网站上,哪怕剧情这一项再出彩,剧情这一项的最高得分也只能达到 1 分,而要想达到 2.5 分,就必须在画面与摄影两个指标上都拿到阈值内的分数才行。Criticality Score 采用了后者,任何高于阈值的 Signal 值都将处于阈值的最大重要性,它们都会被认为是真正 “Critical” 的。

基于以上考量而得出并进行归一化(为保持分数范围有界干脆使其处于[0,1])后得出的计算公式为

formula

Criticality Score Process Abstract

Criticality Score 的数据采集模块与评分模块被解耦为了单独的程序,可以单独执行数据采集,评分,序列化为CSV文件这三项任务,也可以直接使用 Criticality Score 程序将这三项任务一步处理到位。为了方便自动生成开源项目的关键性分数,OpenSSF 还提供了一个枚举 Github 上的开源项目的工具,它的输出可以直接用作 Criticality Score 的输入。为方便简单介绍 Criticality Score 的核心逻辑,下文给出的有关 Criticality Score 具体实现的核心代码将去除其中的异常处理或日志信息记录等基础逻辑。代码摘要如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
func main() {
// 读取命令行参数进行解析
initFlags()

// 获取评分器,其评分逻辑为逐行读取 Signal 的数值与权重计算出结果,再逐行相加,与上述算法描述一致
s := getScorer(logger)
scoreColumnName := generateScoreColumnName(s)

ctx := context.Background()

// 设置每个主机的空闲连接数
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = *workersFlag * 5

// 准备数据采集所需的运行参数
opts := []collector.Option{
collector.EnableAllSources(),
collector.GCPProject(*gcpProjectFlag),
collector.GCPDatasetName(*depsdevDatasetFlag),
collector.GCPDatasetTTL(time.Hour * time.Duration(*depsdevTTLFlag)),
}
if *depsdevDisableFlag {
opts = append(opts, collector.DisableSource(collector.SourceTypeDepsDev))
}

// 创建数据采集器实例
c, err := collector.New(ctx, logger, opts...)

// 创建读取数据用的迭代器
iter, err := inputiter.New(flag.Args())

// 打开输出文件
w, err := outfile.Open(context.Background())

extras := []string{}
if s != nil {
extras = append(extras, scoreColumnName)
}
out := formatType.New(w, c.EmptySets(), extras...)

// 创建数据采集线程池与其通道
repos := make(chan *url.URL)
wait := workerpool.WorkerPool(*workersFlag, func(worker int) {
innerLogger := logger.With(zap.Int("worker", worker))
// 读取通道中的开源项目仓库 URL 并抛入至数据采集线程池进行数据采集
for u := range repos {
l := innerLogger.With(zap.String("url", u.String()))
ss, err := c.Collect(ctx, u, "")

// 根据参数设置情况判断是否输出评分
extras := []signalio.Field{}
if s != nil {
f := signalio.Field{
Key: scoreColumnName,
// Scorer 会逐行读取各 signal 值并按照计算公式求出 Criticality Score
Value: fmt.Sprintf("%.5f", s.Score(ss)),
}
extras = append(extras, f)
}

// 存储 Signal
if err := out.WriteSignals(ss, extras...); err != nil {
l.With(
zap.Error(err),
).Error("Failed to write signal set")
os.Exit(1)
}
}
})

// 从输入中读取仓库地址并通过通道发送至数据采集线程池
for iter.Next() {
line := iter.Item()

u, err := url.Parse(strings.TrimSpace(line))
repos <- u
}
close(repos)
// 主线程阻塞至所有线程工作完成
wait()
}

Criticality Score 的实现逻辑非常简单。作为命令行程序,Criticality Score 首先会解析命令行参数并根据命令行参数以决定程序行为。Criticality Score 实现了一个基于 Go Routine 进行多线程 HTTP 请求以采集所需数据的线程池,并将读取的仓库地址逐个通过 Go 的 Channel 发送至线程池以降低数据采集过程中的网络与文件 I/O 阻塞,进而完成采集任务。在通过命令行参数开启了评分选项时,Criticality Score 还会在每一个项目数据采集结束后读取采集数据计算评分。

争议与展望

在阅读评分算法时很难不感到这一算法未免有些过于简易,事实上,Criticality Score 自问世以来一直面临着比较多的争议。在源代码仓库的 Issue 中这些争议主要包括

OSSF 想将该关键性评分作为类似于学术界的 H-Index 指数的评价指标实现并推广,但其评分算法,评分参考指标甚至是对于关键性的定义都存在着不少的争议,事实上(私以为)也确实欠缺一些更深入的研究与考虑,Criticality Score 的项目具体实现实际上也因此比较简单。但对于软件/资产的重要性/安全性/性能表现进行评估的需求确实始终存在,有待后人提出考虑更周全的评分算法/模型进行更准确而有效的判断。

Reference

Criticality Score
Criticality analysis: What is it and how is it done?
Criticality Analysis for Maintenance Purposes
Criticality Analysis: What It Is and Why It’s Important
What is Criticality Analysis and How to Do it
Criticality Analysis Process Model

  • Title: 对开源项目的关键性评分
  • Author: 7erry
  • Created at : 2024-07-22 23:45:47
  • Updated at : 2024-07-22 23:45:47
  • Link: http://7erry.com/2024/07/22/对开源项目的关键性评分/
  • License: This work is licensed under CC BY-NC 4.0.