
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
bs-common-util
Advanced tools
'use strict'
/**
* 工具类
* util.js
*/
const REG_PHONE = /^\d{11}$/ // 手机号
const REG_VCODE = /^\d{6}$/ // 验证码
const REG_CARDNUMBER = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/ // 身份证号
const REG_CHINESE_NAME = /^[\u4E00-\u9FA5]{2,4}$/ // 中文姓名
const REG_PASSWORD = /^[0-9A-Za-z]{6,20}$/
// 正则手机号码
function testPhone(v) {
return REG_PHONE.test(v)
}
exports.testPhone = testPhone
// 正则短信验证码
function testVcode(v) {
return REG_VCODE.test(v)
}
exports.testVcode = testVcode
// 正则身份证号码
function testCardNumber(v) {
return REG_CARDNUMBER.test(v)
}
exports.testCardNumber = testCardNumber
// 正则登录密码
function testPassword(v) {
return REG_PASSWORD.test(v)
}
exports.testPassword = testPassword
// 正则是否为空
function testEmpty(v) {
return String(v).trim() === '' || v === null
}
exports.testEmpty = testEmpty
// 正则姓名
function testChineseName(v) {
return REG_CHINESE_NAME.test(v)
}
exports.testChineseName = testChineseName
/**
* 安全处理接口返回的message数据
*
* 注:当返回的message为空,可以用自定义 字符串 替换
*
* @param resp 响应数据
* @param replaceMsg 如果返回的 message 为空,则用改值替换
*/
function safeHandleResponseMessage(resp, replaceMsg) {
let replaceMsgStr = replaceMsg ? replaceMsg : '服务异常'
if (!resp) {
return replaceMsgStr
}
if (!resp.message && !resp.msg) {
return replaceMsgStr
}
if (resp.message) {
if (resp.message.length == 0) {
return replaceMsgStr
} else {
return resp.message
}
}
if (resp.msg) {
if (resp.msg.length == 0) {
return replaceMsgStr
} else {
return resp.msg
}
}
return replaceMsgStr
}
exports.safeHandleResponseMessage = safeHandleResponseMessage
/**
* 根据身份证号码获取出生日期
* @param {String} idCard
*/
function getBirthdayFromIdCard(idCard) {
let birthday = ''
if (idCard != null && idCard !== '') {
if (idCard.length === 15) {
birthday = '19' + idCard.substr(6, 6)
} else if (idCard.length === 18) {
birthday = idCard.substr(6, 8)
}
birthday = birthday.replace(/(.{4})(.{2})/, '$1-$2-')
}
return birthday
}
exports.getBirthdayFromIdCard = getBirthdayFromIdCard
/**
* 根据身份证号码获取性别
* @param {String} idCard
*/
function getSexFromIdCard(idCard) {
const len = (idCard + '').length
if (len === 15) {
return idCard.substr(14, 1) % 2 === 1 ? '1' : '2'
} else if (len === 18) {
return idCard.substr(16, 1) % 2 === 1 ? '1' : '2'
}
return '1'
}
exports.getSexFromIdCard = getSexFromIdCard
/**
* 根据身份证号码获取年龄
* @param {String} idCard
*/
function getAgeFromIdCard(idCard) {
const len = (idCard + '').length
let strBirthday = ''
if (len == 18) {
//处理18位的身份证号码从号码中得到生日和性别代码
strBirthday =
idCard.substr(6, 4) +
'/' +
idCard.substr(10, 2) +
'/' +
idCard.substr(12, 2)
}
if (len == 15) {
let birthdayValue = ''
birthdayValue = idCard.charAt(6) + idCard.charAt(7)
if (parseInt(birthdayValue) < 10) {
strBirthday =
'20' +
idCard.substr(6, 2) +
'/' +
idCard.substr(8, 2) +
'/' +
idCard.substr(10, 2)
} else {
strBirthday =
'19' +
idCard.substr(6, 2) +
'/' +
idCard.substr(8, 2) +
'/' +
idCard.substr(10, 2)
}
}
// 时间字符串里,必须是“/”
const birthDate = new Date(strBirthday)
const nowDateTime = new Date()
let age = nowDateTime.getFullYear() - birthDate.getFullYear()
//再考虑月、天的因素;.getMonth()获取的是从0开始的,这里进行比较,不需要加1
if (
nowDateTime.getMonth() < birthDate.getMonth() ||
(nowDateTime.getMonth() == birthDate.getMonth() &&
nowDateTime.getDate() < birthDate.getDate())
) {
age--
}
return age
}
exports.getAgeFromIdCard = getAgeFromIdCard
// Parse the time to string
const parseTime = (time, cFormat) => {
if (time === undefined || !time) {
return null
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if (typeof time === 'string') {
if (/^[0-9]+$/.test(time)) {
// support "1548221490638"
time = parseInt(time)
} else {
// support safari
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
time = time.replace(new RegExp(/-/gm), '/')
}
}
if (typeof time === 'number' && time.toString().length === 10) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay(),
}
const timeStr = format.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value]
}
return value.toString().padStart(2, '0')
})
return timeStr
}
exports.parseTime = parseTime
/**
* 根据日期获取年龄
* @param {String} date
*/
function getAgeFromDate(str) {
const r = str.match(/^(\d{1,4})(-|\/)(\d{1,2})\2(\d{1,2})$/)
if (r == null) return false
const d = new Date(r[1], r[3] - 1, r[4])
if (
d.getFullYear() == r[1] &&
d.getMonth() + 1 == r[3] &&
d.getDate() == r[4]
) {
const Y = new Date().getFullYear()
return Y - r[1]
}
return '输入的日期格式错误!'
}
exports.getAgeFromDate = getAgeFromDate
/**
* 格式化日期
* @param {Date} date
* @param {String} fmt 'yyyy-MM-dd hh:mm:ss'
*/
function formatDate(date, fmt) {
let padLeftZero = (str) => ('00' + str).substr(str.length)
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length)
)
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
}
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + ''
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? str : padLeftZero(str)
)
}
}
return fmt
}
exports.formatDate = formatDate
// 计算剩余时间
function leftTime(time1, time2) {
let syhm = time1 - time2
let day = 0
let hour = 0
let minite = 0
let second = 0
if (syhm > 0) {
day = Math.floor(syhm / 1000 / 60 / 60 / 24)
hour = Math.floor((syhm / 1000 / 60 / 60) % 24)
minite = Math.floor((syhm / 1000 / 60) % 60)
second = Math.floor((syhm / 1000) % 60)
}
return {
day,
hour,
minite,
second,
}
}
exports.leftTime = leftTime
// 获取当前日期
function getCurrentDate() {
let weeks = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
let d = new Date()
let d2 = formatDate(d, 'yyyy年MM月dd日 ')
return d2 + weeks[d.getDay()]
}
exports.getCurrentDate = getCurrentDate
/**
* 获取周
*/
function getCurrentWeek(str) {
let weeks = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
let d = new Date(str.replace(/-/g, '/'))
return weeks[d.getDay()]
}
exports.getCurrentWeek = getCurrentWeek
/**
* 获取时间段
*/
function getCurrentPeriod(str) {
let d = new Date(str.replace(/-/g, '/'))
const hour = d.getHours()
let hourStr = ''
if (hour <= 12) {
hourStr = '上午'
} else if (hour > 12 && hour < 18) {
hourStr = '下午'
} else {
hourStr = '晚上'
}
return hourStr
}
exports.getCurrentPeriod = getCurrentPeriod
/**
* 获取当前日期 yyyy-MM-dd
*/
function getCurrentDateNormal() {
let d = new Date()
let d2 = formatDate(d, 'yyyy-MM-dd')
return d2
}
exports.getCurrentDateNormal = getCurrentDateNormal
/**
* 获取当前日期yyyy-MM-dd
*/
function getNowDate() {
const date = new Date()
let month = date.getMonth() + 1
let strDate = date.getDate()
if (month <= 9) {
month = '0' + month
}
if (strDate <= 9) {
strDate = '0' + strDate
}
return date.getFullYear() + '-' + month + '-' + strDate
}
exports.getNowDate = getNowDate
/**
* 获取昨天日期 yyyy-MM-dd
*/
function getYesterdayDateNormal() {
let d = new Date().setDate(new Date().getDate() - 1)
let d2 = formatDate(new Date(d), 'yyyy-MM-dd')
return d2
}
exports.getYesterdayDateNormal = getYesterdayDateNormal
/**
* 获取当前日期 yyyy-MM-dd hh:mm:ss
*/
function getCurrentDateDetailNormal() {
let d = new Date()
let d2 = formatDate(d, 'yyyy-MM-dd hh:mm:ss')
return d2
}
exports.getCurrentDateDetailNormal = getCurrentDateDetailNormal
/**
* 获取当前年份 yyyy
*/
function getCurrentYearNormal() {
let date = new Date()
return date.getFullYear()
}
exports.getCurrentYearNormal = getCurrentYearNormal
function getWeekName(week) {
let weeks = ['', '周日', '周一', '周二', '周三', '周四', '周五', '周六']
return weeks[week]
}
exports.getWeekName = getWeekName
/**
* 格式化日期
* @param 时间毫秒数
* @param {String} fmt '天、时
*/
function formatDuringHours(mss) {
const days = parseInt(mss / (1000 * 60 * 60 * 24))
const hours = parseInt((mss % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = parseInt((mss % (1000 * 60 * 60)) / (1000 * 60))
const minutesTrue = minutes.toString().length < 2 ? '0' + minutes : minutes
const seconds = parseInt((mss % (1000 * 60)) / 1000)
const secondsTrue = seconds.toString().length < 2 ? '0' + seconds : seconds
const hoursTrue = hours
? hours.toString().length < 2
? '0' + hours
: hours
: ''
return hoursTrue
? days * 24 + hours + '小时'
: minutesTrue
? minutesTrue + '分'
: secondsTrue + '秒'
}
exports.formatDuringHours = formatDuringHours
/**
* 格式化日期
* @param 时间毫秒数
* @param {String} fmt '天、时、分、秒'
*/
function formatDuring(mss) {
const days = parseInt(mss / (1000 * 60 * 60 * 24))
const daysTrue = days ? (days.toString().length < 2 ? '0' + days : days) : ''
const hours = parseInt((mss % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const hoursTrue = hours
? hours.toString().length < 2
? '0' + hours
: hours
: ''
const minutes = parseInt((mss % (1000 * 60 * 60)) / (1000 * 60))
const minutesTrue = minutes.toString().length < 2 ? '0' + minutes : minutes
const seconds = parseInt((mss % (1000 * 60)) / 1000)
const secondsTrue = seconds.toString().length < 2 ? '0' + seconds : seconds
return (
(hoursTrue ? days * 24 + hours + '时' : '') +
(minutesTrue ? minutesTrue + '分' : '') +
secondsTrue +
'秒'
)
}
exports.formatDuring = formatDuring
/**
* 格式化日期
* @param 时间毫秒数
* @param {String} fmt '天、时、分、秒'
*/
function formatDuringNormal(mss) {
const days = parseInt(mss / (1000 * 60 * 60 * 24))
const daysTrue = days ? (days.toString().length < 2 ? '0' + days : days) : ''
const hours = parseInt((mss % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const hoursTrue = hours.toString().length < 2 ? '0' + hours : hours
const minutes = parseInt((mss % (1000 * 60 * 60)) / (1000 * 60))
const minutesTrue = minutes.toString().length < 2 ? '0' + minutes : minutes
const seconds = parseInt((mss % (1000 * 60)) / 1000)
const secondsTrue = seconds.toString().length < 2 ? '0' + seconds : seconds
return (
(daysTrue ? daysTrue + '天' : '') +
hoursTrue +
':' +
(minutesTrue ? minutesTrue + ':' : '') +
secondsTrue
)
}
exports.formatDuringNormal = formatDuringNormal
// Date型数据转换成yyyy-MM-dd
function dateToNormal(date) {
if (date && typeof date === 'object') {
const dateTrue = new Date(date)
return formatDate(dateTrue, 'yyyy-MM-dd')
}
return date
}
exports.dateToNormal = dateToNormal
// 获取日期
function objectDate(date) {
if (date && typeof date === 'string') {
date = new Date(date)
let o = {
Y: date.getFullYear(),
M: date.getMonth() + 1,
D: date.getDate(),
}
return o
}
return date
}
exports.objectDate = objectDate
// 深拷贝
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj))
}
exports.deepCopy = deepCopy
/**
* 获取url参数
*/
function GetRequest(to) {
let url = to || ''
let theRequest = {}
if (url.indexOf('?') !== -1) {
let str = url.split('?')[2]
let strs = str.split('&')
for (let i = 0; i < strs.length; i++) {
theRequest[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1])
}
}
return theRequest
}
exports.GetRequest = GetRequest
/**
* 获取url参数,注意该URL=scheme://host/path?params
*/
function GetRequestOne(to) {
let url = to || ''
let theRequest = {}
if (url.indexOf('?') !== -1) {
let str = url.split('?')[1]
let strs = str.split('&')
for (let i = 0; i < strs.length; i++) {
theRequest[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1])
}
}
return theRequest
}
exports.GetRequestOne = GetRequestOne
// 获取开始和结束时间
function getWeekStartAndEnd(d) {
let now = new Date()
let nowTime = now.getTime()
let day = now.getDay()
let oneday = 60 * 60 * 24 * 1000
let sunday = nowTime - (-1 * d + day) * oneday
let satday = nowTime + (d + 6 - day) * oneday
return {
start: formatDate(new Date(sunday), 'yyyy-MM-dd'),
end: formatDate(new Date(satday), 'yyyy-MM-dd'),
}
}
exports.getWeekStartAndEnd = getWeekStartAndEnd
// 日期之差
function DateDiff(sDate1, sDate2) {
let aDate, oDate1, oDate2, iDays
aDate = sDate1.split('-')
oDate1 = new Date(aDate[1] + '-' + aDate[2] + '-' + aDate[0])
aDate = sDate2.split('-')
oDate2 = new Date(aDate[1] + '-' + aDate[2] + '-' + aDate[0])
iDays = Number((Number(oDate1) - Number(oDate2)) / 1000 / 60 / 60 / 24) // 把相差的毫秒数转换为天数
return iDays
}
exports.DateDiff = DateDiff
// 根据年份获取年龄
function getAgeFromBirthday(birthday) {
if (!birthday) {
return 0
}
let bornYear = birthday.substring(0, 4)
let curYear = new Date().getFullYear()
return curYear - Number(bornYear)
}
exports.getAgeFromBirthday = getAgeFromBirthday
function isWechatBrowser() {
let ua = navigator.userAgent.toLowerCase()
return ua.indexOf('micromessenger') > -1
}
exports.isWechatBrowser = isWechatBrowser
// const browser = {
// versions: (function () {
// const u = navigator.userAgent
// return {
// trident: u.indexOf('Trident') > -1, // IE内核
// presto: u.indexOf('Presto') > -1, // opera内核
// webKit: u.indexOf('AppleWebKit') > -1, // 苹果、谷歌内核
// gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') === -1, // 火狐内核
// mobile: !!u.match(/AppleWebKit.*Mobile.*/), // 是否为移动终端
// ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), // ios终端
// android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, // android终端
// iPhone: u.indexOf('iPhone') > -1, // 是否为iPhone或者QQHD浏览器
// iPad: u.indexOf('iPad') > -1, // 是否iPad
// webApp: u.indexOf('Safari') === -1, // 是否web应该程序,没有头部与底部
// weixin: u.indexOf('MicroMessenger') > -1, // 是否微信 (2015-01-22新增)
// qq: u.match(/\sQQ/i) === ' qq', // 是否QQ
// }
// })(),
// language: (navigator.browserLanguage || navigator.language).toLowerCase(),
// }
// exports.testPhone = testPhone;
// 获取年龄
const formatDateGetAge = (now) => {
let Y = now.getFullYear()
let year = new Date().getFullYear()
return year - Y
}
exports.formatDateGetAge = formatDateGetAge
// 获取时间
function getTime(val) {
if (!val) {
return ''
}
return val.slice(11, 16)
}
exports.getTime = getTime
// 获取支付方式
function filterPayMode(val) {
return ['', '支付宝', '微信'][val]
}
exports.filterPayMode = filterPayMode
// 性别编号
function sexDoc(str) {
if (!str) {
return ''
}
let cardTypesDic = {
男: '1',
女: '2',
}
return cardTypesDic[str]
}
exports.sexDoc = sexDoc
// 国籍编号
function nationaliyDoc(str) {
if (!str) {
return ''
}
let cardTypesDic = {
中国: '01',
中国香港澳门: '02',
中国台湾: '03',
海外: '04',
}
return cardTypesDic[str]
}
exports.nationaliyDoc = nationaliyDoc
// 身份证编号
function idCardTypeDoc(str) {
if (!str) {
return ''
}
let cardTypesDic = {
居民身份证: '01',
居民户口簿: '02',
护照: '03',
军官证: '04',
驾驶证: '05',
港澳居民来往内地通行证: '06',
台湾居民来往内地通行证: '07',
出生证明: '08',
港澳居民身份证: '09',
港澳居民居住证: '10',
其他法定有效证件: '99',
}
return cardTypesDic[str]
}
exports.idCardTypeDoc = idCardTypeDoc
/**
* 根据传过来的字符串 数值
* 来展示当前内容
* @param {String} val
* @param {Number} num
*/
function accordingNumToShow(val, num) {
if (val && val.length > num) {
return val.substr(0, num) + '...'
} else {
return val
}
}
exports.accordingNumToShow = accordingNumToShow
/**
* 根据传过来的两个object
* 判断键值对是否相等
*/
function isObjectValueEqual(a, b) {
const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b)
if (aProps.length != bProps.length) {
return false
}
for (let i = 0; i < aProps.length; i += 1) {
const propName = aProps[i]
const propA = a[propName]
const propB = b[propName]
if (typeof propA === 'object') {
if (isObjectValueEqual(propA, propB)) {
return true
} else {
return false
}
} else if (propA !== propB) {
return false
} else {
return true
}
}
return true
}
exports.isObjectValueEqual = isObjectValueEqual
/**
* 判断当前字符串是否存在
*/
function isStringExist(str) {
if (!str) {
return ''
}
return str
}
exports.isStringExist = isStringExist
// 当前时间
const getTimer = (FAT = 'YMD', FAG = '-', CUR) => {
let date = ''
if (CUR) {
date = new Date(CUR)
} else {
date = new Date()
}
let year = date.getFullYear()
let month =
date.getMonth() < 9
? '0' + (Number(date.getMonth()) + 1)
: date.getMonth() + 1
let day = date.getDate() > 9 ? date.getDate() : '0' + date.getDate()
let hours = date.getHours() > 9 ? date.getHours() : '0' + date.getHours()
let minutes =
date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()
let seconds =
date.getSeconds() > 9 ? date.getSeconds() : '0' + date.getSeconds()
switch (FAT) {
case 'YMD':
return year + FAG + month + FAG + day
case 'HM':
return hours + ':' + minutes
case 'HMS':
return hours + ':' + minutes + ':' + seconds
case 'YMDHMS':
return (
year +
FAG +
month +
FAG +
day +
' ' +
hours +
':' +
minutes +
':' +
seconds
)
default:
return '指令无法识别!'
}
}
exports.getTimer = getTimer
// 判断是否是json字符串
function isJSON(str) {
if (typeof str == 'string') {
try {
const obj = JSON.parse(str)
if (typeof obj == 'object' && obj) {
return true
} else {
return false
}
} catch (e) {
console.log('error:' + str + '!!!' + e)
return false
}
}
console.log('It is not a string!')
return true
}
exports.isJSON = isJSON
// 判断当前字符串是存在
function isStrEmpty(str) {
if (!str) {
return true
}
if (String(str).trim() === '' || str === null) {
return true
}
return false
}
exports.isStrEmpty = isStrEmpty
// 判断当前图片url模式
function getInitImgUrl(url) {
if (url.includes('?')) {
return url + '&length=100&width=100'
} else {
return url + '?length=100&width=100'
}
}
exports.getInitImgUrl = getInitImgUrl
/**
* 金额,只允许2位小数
*/
function amount(value) {
//金额,只允许保留两位小数
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value)
}
exports.amount = amount
let timer, flag
/**
* 节流原理:在一定时间内,只能触发一次
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/
function throttle(func, wait = 500, immediate = true) {
if (immediate) {
if (!flag) {
flag = true
// 如果是立即执行,则在wait毫秒内开始时执行
typeof func === 'function' && func()
timer = setTimeout(() => {
flag = false
}, wait)
}
} else {
if (!flag) {
flag = true
// 如果是非立即执行,则在wait毫秒内的结束处执行
timer = setTimeout(() => {
flag = false
typeof func === 'function' && func()
}, wait)
}
}
}
exports.throttle = throttle
let timeout = null
/**
* 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/
function debounce(func, wait = 500, immediate = false) {
// 清除定时器
if (timeout !== null) clearTimeout(timeout)
// 立即执行,此类情况一般用不到
if (immediate) {
var callNow = !timeout
timeout = setTimeout(function () {
timeout = null
}, wait)
if (callNow) typeof func === 'function' && func()
} else {
// 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法
timeout = setTimeout(function () {
typeof func === 'function' && func()
}, wait)
}
}
exports.debounce = debounce
FAQs
The npm package bs-common-util receives a total of 3 weekly downloads. As such, bs-common-util popularity was classified as not popular.
We found that bs-common-util demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.